From f229c7be6d3ab21387c4758c71063bf7b594dd26 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Mon, 24 Jun 2024 05:54:36 +0300 Subject: [PATCH 01/56] base grpc server finished --- cmd/searcher/main.go | 19 +++++++++ go.mod | 7 ++++ go.sum | 14 +++++++ internal/searcher/app/app.go | 22 ++++++++++ internal/searcher/app/grpc/app.go | 70 +++++++++++++++++++++++++++++++ internal/searcher/grpc/server.go | 27 ++++++++++++ searcher/go.mod | 3 -- 7 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 internal/searcher/app/app.go create mode 100644 internal/searcher/app/grpc/app.go create mode 100644 internal/searcher/grpc/server.go delete mode 100644 searcher/go.mod diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index 06d49c7..032d0e5 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -1,10 +1,13 @@ package main import ( + "github.com/getz-devs/librakeeper-server/internal/searcher/app" "github.com/getz-devs/librakeeper-server/internal/searcher/config" "github.com/getz-devs/librakeeper-server/lib/prettylog" "log/slog" "os" + "os/signal" + "syscall" ) const ( @@ -23,6 +26,22 @@ func main() { slog.Any("config", cfg), slog.Int("port", cfg.GRPC.Port), ) + + application := app.New(log, cfg.GRPC.Port) + go application.GRPCSrv.MustRun() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + + sign := <-stop + + log.Info("shutting down ...", + slog.String("signal", sign.String()), + ) + + application.GRPCSrv.Stop() + + log.Info("application fully stopped") } func setupLogger(env string) *slog.Logger { diff --git a/go.mod b/go.mod index 0b2a0c1..887293c 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,15 @@ go 1.22 require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/getz-devs/librakeeper-protos v0.0.1 // indirect github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect github.com/joho/godotenv v1.5.1 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 20978eb..75b15c0 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,24 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/getz-devs/librakeeper-protos v0.0.1 h1:2iWkQEV2AwCyZJDGlFObYWGHAD+6GTrW2WLlsyni300= +github.com/getz-devs/librakeeper-protos v0.0.1/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/searcher/app/app.go b/internal/searcher/app/app.go new file mode 100644 index 0000000..274b152 --- /dev/null +++ b/internal/searcher/app/app.go @@ -0,0 +1,22 @@ +package app + +import ( + grpcapp "github.com/getz-devs/librakeeper-server/internal/searcher/app/grpc" + "log/slog" +) + +type App struct { + GRPCSrv *grpcapp.App +} + +func New( + log *slog.Logger, + grpcPort int, +) *App { + grpcApp := grpcapp.New(log, grpcPort) + + return &App{ + GRPCSrv: grpcApp, + } + +} diff --git a/internal/searcher/app/grpc/app.go b/internal/searcher/app/grpc/app.go new file mode 100644 index 0000000..b33bbd1 --- /dev/null +++ b/internal/searcher/app/grpc/app.go @@ -0,0 +1,70 @@ +package grpcapp + +import ( + "fmt" + searcherrpc "github.com/getz-devs/librakeeper-server/internal/searcher/grpc" + "google.golang.org/grpc" + "log/slog" + "net" +) + +type App struct { + log *slog.Logger + gRPCServer *grpc.Server + port int +} + +// New creates a new App +func New( + log *slog.Logger, + port int, +) *App { + gRPCServer := grpc.NewServer() + searcherrpc.Register(gRPCServer) + + return &App{ + log: log, + gRPCServer: gRPCServer, + port: port, + } +} + +// MustRun +func (a *App) MustRun() { + if err := a.Run(); err != nil { + panic(err) + } +} + +// Run method +func (a *App) Run() error { + const op = "grpcapp.App.Run" + + log := a.log.With( + slog.String("op", op), + slog.Int("port", a.port), + ) + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) + if err != nil { + return fmt.Errorf("%s ,error when creating listener: %w", op, err) + } + + log.Info("gRPC server is running", + slog.String("address", listener.Addr().String()), + ) + + if err := a.gRPCServer.Serve(listener); err != nil { + return fmt.Errorf("%s ,error when starting gRPC server: %w", op, err) + } + + return nil +} + +// Stop method +func (a *App) Stop() { + const op = "grpcapp.App.Stop" + + a.log.With(slog.String("op", op)).Info("stopping gRPC server") + a.gRPCServer.GracefulStop() +} diff --git a/internal/searcher/grpc/server.go b/internal/searcher/grpc/server.go new file mode 100644 index 0000000..df700ef --- /dev/null +++ b/internal/searcher/grpc/server.go @@ -0,0 +1,27 @@ +package searcher + +import ( + "context" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "google.golang.org/grpc" +) + +type serverAPI struct { + searcherv1.UnimplementedSearcherServer +} + +func Register(gRPC *grpc.Server) { + searcherv1.RegisterSearcherServer(gRPC, &serverAPI{}) +} + +func (*serverAPI) SearchByISBN( + ctx context.Context, + req *searcherv1.SearchByISBNRequest, +) (*searcherv1.SearchByISBNResponse, error) { + return &searcherv1.SearchByISBNResponse{ + Title: "title", + Author: "author", + Publisher: "publisher", + Year: "year", + }, nil +} diff --git a/searcher/go.mod b/searcher/go.mod deleted file mode 100644 index 31f9568..0000000 --- a/searcher/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module searcher - -go 1.22 From b38eec5a00043534562fd0853bb2a41a9e825aa8 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 25 Jun 2024 04:57:38 +0300 Subject: [PATCH 02/56] create api server config and cmd --- cmd/server/main.go | 44 ++++++++++++++++++++++ internal/server/config/config.go | 63 ++++++++++++++++++++++++++++++++ searcher/go.mod | 3 -- 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 cmd/server/main.go create mode 100644 internal/server/config/config.go delete mode 100644 searcher/go.mod diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..f91e030 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "github.com/getz-devs/librakeeper-server/internal/server/config" + "github.com/getz-devs/librakeeper-server/lib/prettylog" + "log/slog" + "os" +) + +const ( + envLocal = "local" + envDev = "dev" + envProd = "prod" +) + +func main() { + cfg := config.MustLoad() + + log := setupLogger(cfg.Env) + + log.Info("starting ...", + slog.String("env", cfg.Env), + slog.Int("port", cfg.ServerConfig.Port), + ) +} + +func setupLogger(env string) *slog.Logger { + var log *slog.Logger + + switch env { + case envLocal: + log = slog.New(prettylog.NewHandler(&slog.HandlerOptions{ + Level: slog.LevelInfo, + AddSource: false, + ReplaceAttr: nil, + })) + case envDev: + log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + case envProd: + log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + } + + return log +} diff --git a/internal/server/config/config.go b/internal/server/config/config.go new file mode 100644 index 0000000..7a44442 --- /dev/null +++ b/internal/server/config/config.go @@ -0,0 +1,63 @@ +package config + +import ( + "flag" + "github.com/ilyakaznacheev/cleanenv" + "os" + "time" +) + +type Config struct { + Env string `yaml:"env" env-default:"local"` + StoragePath string `yaml:"storage_path" env-required:"true"` + SearchConfig SearchConfig `yaml:"search_config"` + ServerConfig ServerConfig `yaml:"server_config"` +} + +type SearchConfig struct { + Port int `yaml:"port" env-default:"44044"` + Timeout time.Duration `yaml:"timeout" env-default:"10h"` +} + +type ServerConfig struct { + Port int `yaml:"port" env-default:"8080"` +} + +func MustLoad() *Config { + path := fetchConfigPath() + if path == "" { + panic("config path is empty") + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + panic("config file doesn't exist: " + path) + } + + var cfg Config + + err := cleanenv.ReadConfig(path, &cfg) + if err != nil { + panic("failed to read config: " + err.Error()) + } + + return &cfg +} + +// fetchConfigPath fetches config path from command line flag or environment variable +// Priority: command line flag > environment variable > default +func fetchConfigPath() string { + var res string + + flag.StringVar(&res, "config", "", "path to config file") + flag.Parse() + + if res == "" { + res = os.Getenv("CONFIG_PATH") + } + + //if res == "" { + // res = "config.yml" + //} + + return res +} diff --git a/searcher/go.mod b/searcher/go.mod deleted file mode 100644 index 31f9568..0000000 --- a/searcher/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module searcher - -go 1.22 From 05f7756c880ddfe671992dcea102828f16617efb Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 25 Jun 2024 06:08:36 +0300 Subject: [PATCH 03/56] demo api server and auth ready --- go.mod | 60 +++++++ go.sum | 248 +++++++++++++++++++++++++++ internal/server/handlers/demo.go | 40 +++++ internal/server/handlers/login.go | 28 +++ internal/server/middlewares/auth.go | 39 +++++ internal/server/server.go | 40 +++++ internal/server/services/firebase.go | 25 +++ 7 files changed, 480 insertions(+) create mode 100644 internal/server/handlers/demo.go create mode 100644 internal/server/handlers/login.go create mode 100644 internal/server/middlewares/auth.go create mode 100644 internal/server/server.go create mode 100644 internal/server/services/firebase.go diff --git a/go.mod b/go.mod index 0b2a0c1..0b22760 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,69 @@ module github.com/getz-devs/librakeeper-server go 1.22 require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/auth v0.5.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/firestore v1.15.0 // indirect + cloud.google.com/go/iam v1.1.8 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect + cloud.google.com/go/storage v1.42.0 // indirect + firebase.google.com/go v3.13.0+incompatible // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/bytedance/sonic v1.11.9 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/gin-contrib/cors v1.7.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.185.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index 20978eb..0e95352 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,260 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/auth v0.5.2 h1:xzzYbfrv7xI5oPzzu11RT66GnNhRrWcVG9TXEVxx86Y= +cloud.google.com/go/auth v0.5.2/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute v1.27.0 h1:EGawh2RUnfHT5g8f/FX3Ds6KZuIBC77hZoDrBvEZw94= +cloud.google.com/go/compute v1.27.0/go.mod h1:LG5HwRmWFKM2C5XxHRiNzkLLXW48WwvyVC0mfWsYPOM= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= +cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go/storage v1.42.0 h1:4QtGpplCVt1wz6g5o1ifXd656P5z+yNgzdw1tVfp0cU= +cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0SO/Vn6JFQ= +firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= +firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= +github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.185.0 h1:ENEKk1k4jW8SmmaT6RE+ZasxmxezCrD5Vw4npvr+pAU= +google.golang.org/api v0.185.0/go.mod h1:HNfvIkJGlgrIlrbYkAm9W9IdkmKZjOTVh33YltygGbg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= +google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= +google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= +google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/server/handlers/demo.go b/internal/server/handlers/demo.go new file mode 100644 index 0000000..061af56 --- /dev/null +++ b/internal/server/handlers/demo.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +func DemoHandler(c *gin.Context) { + foo := c.Query("foo") + if foo != "bar" { + log.Printf("foo: %v", foo) + c.JSON(http.StatusTeapot, gin.H{"error": "Not Happy :("}) + return + } + + c.JSON(http.StatusOK, gin.H{"info": "Happy :)"}) +} + +/* +func CreateTodo(c *gin.Context) { + uid, exists := c.Get("uid") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var body struct { + Todo string `json:"todo"` + } + if err := c.BindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + // Save the todo for the user with UID 'uid' + c.JSON(http.StatusOK, gin.H{"todo": body.Todo}) +} +*/ diff --git a/internal/server/handlers/login.go b/internal/server/handlers/login.go new file mode 100644 index 0000000..ef06ec0 --- /dev/null +++ b/internal/server/handlers/login.go @@ -0,0 +1,28 @@ +package handlers + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "net/http" + + "github.com/gin-gonic/gin" +) + +func LoginHandler(c *gin.Context) { + var body struct { + Token string `json:"token"` + } + + if err := c.BindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + token, err := services.VerifyIDToken(context.Background(), body.Token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + return + } + + c.JSON(http.StatusOK, gin.H{"uid": token.UID}) +} diff --git a/internal/server/middlewares/auth.go b/internal/server/middlewares/auth.go new file mode 100644 index 0000000..38d934d --- /dev/null +++ b/internal/server/middlewares/auth.go @@ -0,0 +1,39 @@ +package middlewares + +import ( + "context" + "net/http" + "strings" + + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/gin-gonic/gin" +) + +func AuthMiddleware(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } + + // Strip the "Bearer " prefix from the Authorization header value + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) + c.Abort() + return + } + idToken := strings.TrimPrefix(authHeader, "Bearer ") + + // Verify the ID token + token, err := services.VerifyIDToken(context.Background(), idToken) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + // Set the UID in the context for further use + c.Set("uid", token.UID) + c.Next() +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..c88befe --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/getz-devs/librakeeper-server/internal/server/handlers" + "github.com/getz-devs/librakeeper-server/internal/server/middlewares" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "log" +) + +func main() { + r := gin.Default() + + // Configure CORS + config := cors.Config{ + AllowOrigins: []string{"http://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization + AllowCredentials: true, // Allow credentials + } + + r.Use(cors.New(config)) + + // Initialize Firebase + err := services.InitializeFirebase("internal/server/.secret.json") // TODO: read config + if err != nil { + log.Fatalf("error initializing Firebase: %v", err) + } + + // Routes + r.POST("/login", handlers.LoginHandler) + r.GET("/demo", middlewares.AuthMiddleware, handlers.DemoHandler) + + err = r.Run(":8080") // TODO: read config + if err != nil { + log.Fatalf("error starting server: %v", err) + return + } +} diff --git a/internal/server/services/firebase.go b/internal/server/services/firebase.go new file mode 100644 index 0000000..b7db416 --- /dev/null +++ b/internal/server/services/firebase.go @@ -0,0 +1,25 @@ +package services + +import ( + "context" + + "firebase.google.com/go" + "firebase.google.com/go/auth" + "google.golang.org/api/option" +) + +var client *auth.Client + +func InitializeFirebase(credentialPath string) error { + opt := option.WithCredentialsFile(credentialPath) + app, err := firebase.NewApp(context.Background(), nil, opt) + if err != nil { + return err + } + client, err = app.Auth(context.Background()) + return err +} + +func VerifyIDToken(ctx context.Context, idToken string) (*auth.Token, error) { + return client.VerifyIDToken(ctx, idToken) +} From f47260bbaa2a93629b158a955f049e6df0695c66 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:06:58 +0300 Subject: [PATCH 04/56] bd try --- go.mod | 16 ++++++ go.sum | 57 +++++++++++++++++++ internal/searcher/domain/models/books.go | 11 ++++ .../searcher/services/searcher/searcher.go | 41 +++++++++++++ internal/searcher/storage/mongo/mongo.go | 19 +++++++ internal/searcher/storage/storage.go | 3 + 6 files changed, 147 insertions(+) create mode 100644 internal/searcher/domain/models/books.go create mode 100644 internal/searcher/services/searcher/searcher.go create mode 100644 internal/searcher/storage/mongo/mongo.go create mode 100644 internal/searcher/storage/storage.go diff --git a/go.mod b/go.mod index 887293c..9df0dcd 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,26 @@ go 1.22 require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/getz-devs/librakeeper-protos v0.0.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/golang/snappy v0.0.1 // indirect github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/kamva/mgm/v3 v3.5.0 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.6.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.0.2 // indirect + github.com/xdg-go/stringprep v1.0.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.mongodb.org/mongo-driver v1.8.3 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect diff --git a/go.sum b/go.sum index 75b15c0..f862320 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,73 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getz-devs/librakeeper-protos v0.0.1 h1:2iWkQEV2AwCyZJDGlFObYWGHAD+6GTrW2WLlsyni300= github.com/getz-devs/librakeeper-protos v0.0.1/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kamva/mgm/v3 v3.5.0 h1:/2mNshpqwAC9spdzJZ0VR/UZ/SY/PsNTrMjT111KQjM= +github.com/kamva/mgm/v3 v3.5.0/go.mod h1:F4J1hZnXQMkqL3DZgR7Z7BOuiTqQG/JTic3YzliG4jk= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= +go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= @@ -20,6 +75,8 @@ google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLp google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= diff --git a/internal/searcher/domain/models/books.go b/internal/searcher/domain/models/books.go new file mode 100644 index 0000000..80ced5c --- /dev/null +++ b/internal/searcher/domain/models/books.go @@ -0,0 +1,11 @@ +package models + +type BookSearchResult struct { + ISBN string + Title string + Author string + Publisher string + Year string +} + +type BooksSearchResult []BookSearchResult diff --git a/internal/searcher/services/searcher/searcher.go b/internal/searcher/services/searcher/searcher.go new file mode 100644 index 0000000..edad651 --- /dev/null +++ b/internal/searcher/services/searcher/searcher.go @@ -0,0 +1,41 @@ +package searcher + +import ( + "context" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/searcher/domain/models" + "log/slog" +) + +type SearcherService struct { + log *slog.Logger + ISBNSearcher ISBNSearcher +} + +// ISBNSearcher return models.BooksSearchResult +type ISBNSearcher interface { + SearchByISBN(ctx context.Context, isbn string) (models.BooksSearchResult, error) +} + +func New(log *slog.Logger, ISBNSearcher ISBNSearcher) *SearcherService { + return &SearcherService{ + log: log, + ISBNSearcher: ISBNSearcher, + } +} + +func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (models.BooksSearchResult, error) { + const op = "searcher.SearcherService.SearchByISBN" + s.log.With( + slog.String("op", op), + slog.String("isbn", isbn), + ).Info("searching by ISBN") + + data, err := s.ISBNSearcher.SearchByISBN(ctx, isbn) + if err != nil { + s.log.Error("error when searching by ISBN", err) + return nil, fmt.Errorf("%s ,error when searching by ISBN: %w", op, err) + } + + return data, nil +} diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go new file mode 100644 index 0000000..3a1a585 --- /dev/null +++ b/internal/searcher/storage/mongo/mongo.go @@ -0,0 +1,19 @@ +package mongo + +import ( + "github.com/kamva/mgm/v3" +) + +type Storage struct { + db *mgm.Collection +} + +func New(connectUrl string) *Storage { + const op = "storage.mongo.New" + + db, er = mgm.NewClient() + + return &Storage{ + db: db, + } +} diff --git a/internal/searcher/storage/storage.go b/internal/searcher/storage/storage.go new file mode 100644 index 0000000..0309a42 --- /dev/null +++ b/internal/searcher/storage/storage.go @@ -0,0 +1,3 @@ +package storage + +//type Storage From 46134df8adaac6ce7e5544c39a59f1fbcdecf3dd Mon Sep 17 00:00:00 2001 From: DingoPortable <26508358+Sergeydigl3@users.noreply.github.com> Date: Fri, 28 Jun 2024 05:02:43 +0300 Subject: [PATCH 05/56] storage finished? --- cmd/searcher/main.go | 2 +- go.mod | 1 - internal/searcher/app/app.go | 6 +- internal/searcher/app/grpc/app.go | 4 +- internal/searcher/config/config.go | 8 ++ internal/searcher/domain/models/books.go | 2 +- internal/searcher/grpc/server.go | 12 ++- .../searcher/services/searcher/searcher.go | 6 +- internal/searcher/storage/mongo/mongo.go | 76 +++++++++++++++++-- 9 files changed, 102 insertions(+), 15 deletions(-) diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index 032d0e5..bb55944 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -21,7 +21,7 @@ func main() { log := setupLogger(cfg.Env) - log.Info("starting ...", + log.Info("startingg ...", slog.String("env", cfg.Env), slog.Any("config", cfg), slog.Int("port", cfg.GRPC.Port), diff --git a/go.mod b/go.mod index 9df0dcd..ff5fdd9 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/joho/godotenv v1.5.1 // indirect - github.com/kamva/mgm/v3 v3.5.0 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/internal/searcher/app/app.go b/internal/searcher/app/app.go index 274b152..13d2b7f 100644 --- a/internal/searcher/app/app.go +++ b/internal/searcher/app/app.go @@ -2,6 +2,8 @@ package app import ( grpcapp "github.com/getz-devs/librakeeper-server/internal/searcher/app/grpc" + "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" + mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher/storage/mongo" "log/slog" ) @@ -13,7 +15,9 @@ func New( log *slog.Logger, grpcPort int, ) *App { - grpcApp := grpcapp.New(log, grpcPort) + storage := mongostorage.New("mongodb://localhost:27017") + searcherService := searcher_service.New(log, storage) + grpcApp := grpcapp.New(log, searcherService, grpcPort) return &App{ GRPCSrv: grpcApp, diff --git a/internal/searcher/app/grpc/app.go b/internal/searcher/app/grpc/app.go index b33bbd1..d80ca43 100644 --- a/internal/searcher/app/grpc/app.go +++ b/internal/searcher/app/grpc/app.go @@ -3,6 +3,7 @@ package grpcapp import ( "fmt" searcherrpc "github.com/getz-devs/librakeeper-server/internal/searcher/grpc" + searcher_service "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" "google.golang.org/grpc" "log/slog" "net" @@ -17,10 +18,11 @@ type App struct { // New creates a new App func New( log *slog.Logger, + searchService *searcher_service.SearcherService, port int, ) *App { gRPCServer := grpc.NewServer() - searcherrpc.Register(gRPCServer) + searcherrpc.Register(gRPCServer, searchService) return &App{ log: log, diff --git a/internal/searcher/config/config.go b/internal/searcher/config/config.go index ba34b8a..ce3a20f 100644 --- a/internal/searcher/config/config.go +++ b/internal/searcher/config/config.go @@ -13,6 +13,8 @@ type Config struct { StoragePath string `yaml:"storage_path" env-required:"true"` GRPC GRPCConfig `yaml:"grpc"` + + DatabaseMongo DatabaseMongoConfig `yaml:"database_mongo"` } type GRPCConfig struct { @@ -20,6 +22,12 @@ type GRPCConfig struct { Timeout time.Duration `yaml:"timeout" env-default:"10h"` } +type DatabaseMongoConfig struct { + ConnectURL string `yaml:"connect_url" env-required:"true"` + DatabaseName string `yaml:"database_name" env-required:"true"` + CollectionName string `yaml:"collection_name_books" env-required:"true"` +} + func MustLoad() *Config { path := fetchConfigPath() if path == "" { diff --git a/internal/searcher/domain/models/books.go b/internal/searcher/domain/models/books.go index 80ced5c..7628200 100644 --- a/internal/searcher/domain/models/books.go +++ b/internal/searcher/domain/models/books.go @@ -8,4 +8,4 @@ type BookSearchResult struct { Year string } -type BooksSearchResult []BookSearchResult +type BooksSearchResult []*BookSearchResult diff --git a/internal/searcher/grpc/server.go b/internal/searcher/grpc/server.go index df700ef..575f0a0 100644 --- a/internal/searcher/grpc/server.go +++ b/internal/searcher/grpc/server.go @@ -3,21 +3,29 @@ package searcher import ( "context" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + searcher_service "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type serverAPI struct { searcherv1.UnimplementedSearcherServer + searcherService *searcher_service.SearcherService } -func Register(gRPC *grpc.Server) { - searcherv1.RegisterSearcherServer(gRPC, &serverAPI{}) +func Register(gRPC *grpc.Server, searcherService *searcher_service.SearcherService) { + searcherv1.RegisterSearcherServer(gRPC, &serverAPI{searcherService: searcherService}) } func (*serverAPI) SearchByISBN( ctx context.Context, req *searcherv1.SearchByISBNRequest, ) (*searcherv1.SearchByISBNResponse, error) { + if req.GetIsbn() == "" { + return nil, status.Error(codes.InvalidArgument, "isbn cannot be empty") + } + return &searcherv1.SearchByISBNResponse{ Title: "title", Author: "author", diff --git a/internal/searcher/services/searcher/searcher.go b/internal/searcher/services/searcher/searcher.go index edad651..9e0110a 100644 --- a/internal/searcher/services/searcher/searcher.go +++ b/internal/searcher/services/searcher/searcher.go @@ -1,4 +1,4 @@ -package searcher +package searcher_service import ( "context" @@ -14,7 +14,7 @@ type SearcherService struct { // ISBNSearcher return models.BooksSearchResult type ISBNSearcher interface { - SearchByISBN(ctx context.Context, isbn string) (models.BooksSearchResult, error) + SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) } func New(log *slog.Logger, ISBNSearcher ISBNSearcher) *SearcherService { @@ -24,7 +24,7 @@ func New(log *slog.Logger, ISBNSearcher ISBNSearcher) *SearcherService { } } -func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (models.BooksSearchResult, error) { +func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { const op = "searcher.SearcherService.SearchByISBN" s.log.With( slog.String("op", op), diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go index 3a1a585..4760cd7 100644 --- a/internal/searcher/storage/mongo/mongo.go +++ b/internal/searcher/storage/mongo/mongo.go @@ -1,19 +1,85 @@ -package mongo +package mongostorage import ( - "github.com/kamva/mgm/v3" + "context" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/searcher/domain/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log" ) type Storage struct { - db *mgm.Collection + client *mongo.Client + col *mongo.Collection } func New(connectUrl string) *Storage { const op = "storage.mongo.New" - db, er = mgm.NewClient() + //db, er = mgm.NewClient() + + client, err := mongo.Connect(context.TODO(), options.Client(). + ApplyURI(connectUrl)) + if err != nil { + panic(err) + } + + coll := client.Database("sample_mflix").Collection("movies") return &Storage{ - db: db, + client: client, + col: coll, + } +} + +func (s *Storage) Close() error { + const op = "storage.mongo.Close" + if err := s.client.Disconnect(context.TODO()); err != nil { + panic(err) } + return nil +} + +func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { + const op = "storage.mongo.FindByISBN" + + // Pass these options to the Find method + findOptions := options.Find() + findOptions.SetLimit(5) + + // Here's an array in which you can store the decoded documents + var results models.BooksSearchResult + + // Passing bson.D{{}} as the filter matches all documents in the collection + cur, err := s.col.Find(context.TODO(), bson.D{{}}, findOptions) + if err != nil { + log.Fatal(err) + } + + // Finding multiple documents returns a cursor + // Iterating through the cursor allows us to decode documents one at a time + for cur.Next(context.TODO()) { + + // create a value into which the single document can be decoded + var elem models.BookSearchResult + err := cur.Decode(&elem) + if err != nil { + log.Fatal(err) + } + + results = append(results, &elem) + } + + if err := cur.Err(); err != nil { + return nil, fmt.Errorf("%s ,error when reading cursor: %w", op, err) + } + + // Close the cursor once finished + if err := cur.Close(ctx); err != nil { + return nil, fmt.Errorf("%s ,error when closing cursor: %w", op, err) + } + + return &results, nil } From bb76ebb4dda01266f33d82f4f4f13f1766d9245b Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Fri, 28 Jun 2024 06:08:45 +0300 Subject: [PATCH 06/56] db connected --- cmd/searcher/main.go | 11 +++++- config/searcher/local.yaml.example | 11 ++++++ docs/searcher/README.MD | 0 go.mod | 16 ++++----- go.sum | 16 +++++---- internal/searcher/app/app.go | 5 ++- internal/searcher/app/grpc/app.go | 35 ++++++++++++++++++- .../searcher/services/searcher/searcher.go | 3 +- internal/searcher/storage/mongo/mongo.go | 17 +++++---- 9 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 config/searcher/local.yaml.example create mode 100644 docs/searcher/README.MD diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index bb55944..e985474 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/getz-devs/librakeeper-server/internal/searcher/app" "github.com/getz-devs/librakeeper-server/internal/searcher/config" + mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher/storage/mongo" "github.com/getz-devs/librakeeper-server/lib/prettylog" "log/slog" "os" @@ -27,7 +28,13 @@ func main() { slog.Int("port", cfg.GRPC.Port), ) - application := app.New(log, cfg.GRPC.Port) + databaseMongoConfig := mongostorage.DatabaseMongoConfig{ + ConnectUrl: cfg.DatabaseMongo.ConnectURL, + Database: cfg.DatabaseMongo.DatabaseName, + Collection: cfg.DatabaseMongo.CollectionName, + } + + application := app.New(log, cfg.GRPC.Port, databaseMongoConfig) go application.GRPCSrv.MustRun() stop := make(chan os.Signal, 1) @@ -41,6 +48,8 @@ func main() { application.GRPCSrv.Stop() + application.Storage.Close() + log.Info("application fully stopped") } diff --git a/config/searcher/local.yaml.example b/config/searcher/local.yaml.example new file mode 100644 index 0000000..974bfb4 --- /dev/null +++ b/config/searcher/local.yaml.example @@ -0,0 +1,11 @@ +env: "local" + +storage_path: "./storage/sso.db" +grpc: + port: 44044 + timeout: 10h + +database_mongo: + connect_url: "mongodb://192.168.1.199:27017/" + database_name: "TempData" + collection_name_books: "books" \ No newline at end of file diff --git a/docs/searcher/README.MD b/docs/searcher/README.MD new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod index ff5fdd9..539063a 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,31 @@ module github.com/getz-devs/librakeeper-server go 1.22 +require ( + github.com/getz-devs/librakeeper-protos v0.0.1 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + go.mongodb.org/mongo-driver v1.8.3 + google.golang.org/grpc v1.64.0 +) + require ( github.com/BurntSushi/toml v1.4.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/getz-devs/librakeeper-protos v0.0.1 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/golang/snappy v0.0.1 // indirect - github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.6.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - go.mongodb.org/mongo-driver v1.8.3 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect - google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect diff --git a/go.sum b/go.sum index f862320..09c1d9c 100644 --- a/go.sum +++ b/go.sum @@ -11,18 +11,20 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kamva/mgm/v3 v3.5.0 h1:/2mNshpqwAC9spdzJZ0VR/UZ/SY/PsNTrMjT111KQjM= -github.com/kamva/mgm/v3 v3.5.0/go.mod h1:F4J1hZnXQMkqL3DZgR7Z7BOuiTqQG/JTic3YzliG4jk= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -30,9 +32,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -75,6 +78,7 @@ google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLp google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/searcher/app/app.go b/internal/searcher/app/app.go index 13d2b7f..499a6d6 100644 --- a/internal/searcher/app/app.go +++ b/internal/searcher/app/app.go @@ -9,18 +9,21 @@ import ( type App struct { GRPCSrv *grpcapp.App + Storage *mongostorage.Storage } func New( log *slog.Logger, grpcPort int, + databaseMongoConfig mongostorage.DatabaseMongoConfig, ) *App { - storage := mongostorage.New("mongodb://localhost:27017") + storage := mongostorage.New(databaseMongoConfig) searcherService := searcher_service.New(log, storage) grpcApp := grpcapp.New(log, searcherService, grpcPort) return &App{ GRPCSrv: grpcApp, + Storage: storage, } } diff --git a/internal/searcher/app/grpc/app.go b/internal/searcher/app/grpc/app.go index d80ca43..46a63dc 100644 --- a/internal/searcher/app/grpc/app.go +++ b/internal/searcher/app/grpc/app.go @@ -1,12 +1,18 @@ package grpcapp import ( + "context" "fmt" searcherrpc "github.com/getz-devs/librakeeper-server/internal/searcher/grpc" searcher_service "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "log/slog" "net" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" ) type App struct { @@ -21,7 +27,26 @@ func New( searchService *searcher_service.SearcherService, port int, ) *App { - gRPCServer := grpc.NewServer() + loggingOpts := []logging.Option{ + logging.WithLogOnEvents( + //logging.StartCall, logging.FinishCall, + logging.PayloadReceived, logging.PayloadSent, + ), + // Add any other option (check functions starting with logging.With). + } + + recoveryOpts := []recovery.Option{ + recovery.WithRecoveryHandler(func(p interface{}) (err error) { + log.Error("Recovered from panic", slog.Any("panic", p)) + + return status.Errorf(codes.Internal, "internal error") + }), + } + + gRPCServer := grpc.NewServer(grpc.ChainUnaryInterceptor( + recovery.UnaryServerInterceptor(recoveryOpts...), + logging.UnaryServerInterceptor(InterceptorLogger(log), loggingOpts...), + )) searcherrpc.Register(gRPCServer, searchService) return &App{ @@ -31,6 +56,14 @@ func New( } } +// InterceptorLogger adapts slog logger to interceptor logger. +// This code is simple enough to be copied and not imported. +func InterceptorLogger(l *slog.Logger) logging.Logger { + return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { + l.Log(ctx, slog.Level(lvl), msg, fields...) + }) +} + // MustRun func (a *App) MustRun() { if err := a.Run(); err != nil { diff --git a/internal/searcher/services/searcher/searcher.go b/internal/searcher/services/searcher/searcher.go index 9e0110a..f6561c5 100644 --- a/internal/searcher/services/searcher/searcher.go +++ b/internal/searcher/services/searcher/searcher.go @@ -29,7 +29,8 @@ func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (*model s.log.With( slog.String("op", op), slog.String("isbn", isbn), - ).Info("searching by ISBN") + ) + s.log.Info("searching by ISBN") data, err := s.ISBNSearcher.SearchByISBN(ctx, isbn) if err != nil { diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go index 4760cd7..ad2b107 100644 --- a/internal/searcher/storage/mongo/mongo.go +++ b/internal/searcher/storage/mongo/mongo.go @@ -15,18 +15,22 @@ type Storage struct { col *mongo.Collection } -func New(connectUrl string) *Storage { - const op = "storage.mongo.New" +type DatabaseMongoConfig struct { + ConnectUrl string + Database string + Collection string +} - //db, er = mgm.NewClient() +func New(databaseConfig DatabaseMongoConfig) *Storage { + const op = "storage.mongo.New" client, err := mongo.Connect(context.TODO(), options.Client(). - ApplyURI(connectUrl)) + ApplyURI(databaseConfig.ConnectUrl)) if err != nil { panic(err) } - coll := client.Database("sample_mflix").Collection("movies") + coll := client.Database(databaseConfig.Database).Collection(databaseConfig.Collection) return &Storage{ client: client, @@ -34,12 +38,11 @@ func New(connectUrl string) *Storage { } } -func (s *Storage) Close() error { +func (s *Storage) Close() { const op = "storage.mongo.Close" if err := s.client.Disconnect(context.TODO()); err != nil { panic(err) } - return nil } func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { From 40dbae3a0cff16d7d6f0e5d2554de3e1e9e76cd7 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:41:14 +0300 Subject: [PATCH 07/56] storage working with new proto --- cmd/searcher/main.go | 2 +- go.mod | 15 +++++- go.sum | 65 ++++++++++++++++++++++++ internal/searcher/app/grpc/app.go | 2 +- internal/searcher/grpc/server.go | 19 ++++--- internal/searcher/storage/mongo/mongo.go | 4 +- 6 files changed, 95 insertions(+), 12 deletions(-) diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index e985474..4f0ea40 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -1,4 +1,4 @@ -package main +package searcher_cmd import ( "github.com/getz-devs/librakeeper-server/internal/searcher/app" diff --git a/go.mod b/go.mod index 539063a..6253fa7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/getz-devs/librakeeper-server go 1.22 require ( - github.com/getz-devs/librakeeper-protos v0.0.1 + github.com/getz-devs/librakeeper-protos v0.0.2 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 github.com/ilyakaznacheev/cleanenv v1.5.0 go.mongodb.org/mongo-driver v1.8.3 @@ -12,11 +12,23 @@ require ( require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/PuerkitoBio/goquery v1.9.2 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/antchfx/htmlquery v1.3.2 // indirect + github.com/antchfx/xmlquery v1.4.1 // indirect + github.com/antchfx/xpath v1.3.1 // indirect github.com/go-stack/stack v1.8.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gocolly/colly v1.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/kennygrant/sanitize v1.2.4 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/temoto/robotstxt v1.1.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect @@ -26,6 +38,7 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 09c1d9c..68fceee 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,39 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/antchfx/htmlquery v1.3.2 h1:85YdttVkR1rAY+Oiv/nKI4FCimID+NXhDn82kz3mEvs= +github.com/antchfx/htmlquery v1.3.2/go.mod h1:1mbkcEgEarAokJiWhTfr4hR06w/q2ZZjnYLrDt6CTUk= +github.com/antchfx/xmlquery v1.4.1 h1:YgpSwbeWvLp557YFTi8E3z6t6/hYjmFEtiEKbDfEbl0= +github.com/antchfx/xmlquery v1.4.1/go.mod h1:lKezcT8ELGt8kW5L+ckFMTbgdR61/odpPgDv8Gvi1fI= +github.com/antchfx/xpath v1.3.1 h1:PNbFuUqHwWl0xRjvUPjJ95Agbmdj2uzzIwmQKgu4oCk= +github.com/antchfx/xpath v1.3.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/getz-devs/librakeeper-protos v0.0.1 h1:2iWkQEV2AwCyZJDGlFObYWGHAD+6GTrW2WLlsyni300= github.com/getz-devs/librakeeper-protos v0.0.1/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= +github.com/getz-devs/librakeeper-protos v0.0.2 h1:NbFBVwR5nx1b5BIxx5epKNePEdMcn5CBUN6kF1bgAa0= +github.com/getz-devs/librakeeper-protos v0.0.2/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= +github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= @@ -19,6 +42,8 @@ github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2l github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -31,10 +56,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= +github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -45,36 +75,71 @@ github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyh github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/searcher/app/grpc/app.go b/internal/searcher/app/grpc/app.go index 46a63dc..c9df41c 100644 --- a/internal/searcher/app/grpc/app.go +++ b/internal/searcher/app/grpc/app.go @@ -47,7 +47,7 @@ func New( recovery.UnaryServerInterceptor(recoveryOpts...), logging.UnaryServerInterceptor(InterceptorLogger(log), loggingOpts...), )) - searcherrpc.Register(gRPCServer, searchService) + searcherrpc.Register(gRPCServer, searchService, log) return &App{ log: log, diff --git a/internal/searcher/grpc/server.go b/internal/searcher/grpc/server.go index 575f0a0..0b26064 100644 --- a/internal/searcher/grpc/server.go +++ b/internal/searcher/grpc/server.go @@ -7,29 +7,34 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "log/slog" ) type serverAPI struct { searcherv1.UnimplementedSearcherServer searcherService *searcher_service.SearcherService + log *slog.Logger } -func Register(gRPC *grpc.Server, searcherService *searcher_service.SearcherService) { - searcherv1.RegisterSearcherServer(gRPC, &serverAPI{searcherService: searcherService}) +func Register(gRPC *grpc.Server, searcherService *searcher_service.SearcherService, log *slog.Logger) { + searcherv1.RegisterSearcherServer(gRPC, &serverAPI{searcherService: searcherService, log: log}) } -func (*serverAPI) SearchByISBN( +func (s *serverAPI) SearchByISBN( ctx context.Context, req *searcherv1.SearchByISBNRequest, ) (*searcherv1.SearchByISBNResponse, error) { if req.GetIsbn() == "" { return nil, status.Error(codes.InvalidArgument, "isbn cannot be empty") } + results, err := s.searcherService.SearchByISBN(ctx, req.GetIsbn()) + if err != nil { + return nil, err + } + s.log.Info("Results", slog.Any("results", results)) return &searcherv1.SearchByISBNResponse{ - Title: "title", - Author: "author", - Publisher: "publisher", - Year: "year", + Status: searcherv1.SearchByISBNResponse_SUCCESS, + Books: []*searcherv1.Book{}, }, nil } diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go index ad2b107..6d1fb1f 100644 --- a/internal/searcher/storage/mongo/mongo.go +++ b/internal/searcher/storage/mongo/mongo.go @@ -56,14 +56,14 @@ func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksS var results models.BooksSearchResult // Passing bson.D{{}} as the filter matches all documents in the collection - cur, err := s.col.Find(context.TODO(), bson.D{{}}, findOptions) + cur, err := s.col.Find(ctx, bson.D{{}}, findOptions) if err != nil { log.Fatal(err) } // Finding multiple documents returns a cursor // Iterating through the cursor allows us to decode documents one at a time - for cur.Next(context.TODO()) { + for cur.Next(ctx) { // create a value into which the single document can be decoded var elem models.BookSearchResult From 9d46d935cd3e731c89c805922c4d48cf22751302 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:41:37 +0300 Subject: [PATCH 08/56] started searcher-agent --- cmd/searcher-agent/main.go | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 cmd/searcher-agent/main.go diff --git a/cmd/searcher-agent/main.go b/cmd/searcher-agent/main.go new file mode 100644 index 0000000..79fa52f --- /dev/null +++ b/cmd/searcher-agent/main.go @@ -0,0 +1,44 @@ +package searcher_agent_cmd + +import ( + "fmt" + colly "github.com/gocolly/colly" +) + +func main() { + // TODO + err := scrapISBNFindBook("9785206000344") + if err != nil { + return + } +} + +var ( + const findbookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" +) +func scrapISBNFindBook(isbn string) error { + c := colly.NewCollector() + c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" + c.OnHTML( + "section.container section.results", + func(e *colly.HTMLElement) { + //fmt.Println(e.Attr("href")) + results_book := e.ChildText("") + + // TODO + }, + ) + + preparedUrl := fmt.Sprintf(findbookUrlTemplate, isbn) + err := c.Visit(preparedUrl) + if err != nil { + return err + } + + + return "" +} + +type book_result struct { + // TODO +} From 1d3bf7f3ff9da3a6b5b74b1be38e46a84ea2cf7a Mon Sep 17 00:00:00 2001 From: DingoPortable <26508358+Sergeydigl3@users.noreply.github.com> Date: Sat, 29 Jun 2024 23:39:20 +0300 Subject: [PATCH 09/56] moved setupLogger to utils pretty log --- cmd/searcher/main.go | 32 +++++--------------------------- lib/prettylog/util.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 lib/prettylog/util.go diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index 4f0ea40..cba6f60 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -1,4 +1,4 @@ -package searcher_cmd +package main import ( "github.com/getz-devs/librakeeper-server/internal/searcher/app" @@ -11,16 +11,10 @@ import ( "syscall" ) -const ( - envLocal = "local" - envDev = "dev" - envProd = "prod" -) - func main() { cfg := config.MustLoad() - log := setupLogger(cfg.Env) + log := prettylog.SetupLogger(cfg.Env) log.Info("startingg ...", slog.String("env", cfg.Env), @@ -34,12 +28,15 @@ func main() { Collection: cfg.DatabaseMongo.CollectionName, } + // --------------------------- Start Application server ----------------------- application := app.New(log, cfg.GRPC.Port, databaseMongoConfig) go application.GRPCSrv.MustRun() + // --------------------------- Register stop signal --------------------------- stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + // --------------------------- Wait for stop signal --------------------------- sign := <-stop log.Info("shutting down ...", @@ -52,22 +49,3 @@ func main() { log.Info("application fully stopped") } - -func setupLogger(env string) *slog.Logger { - var log *slog.Logger - - switch env { - case envLocal: - log = slog.New(prettylog.NewHandler(&slog.HandlerOptions{ - Level: slog.LevelInfo, - AddSource: false, - ReplaceAttr: nil, - })) - case envDev: - log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - case envProd: - log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) - } - - return log -} diff --git a/lib/prettylog/util.go b/lib/prettylog/util.go new file mode 100644 index 0000000..cfecce7 --- /dev/null +++ b/lib/prettylog/util.go @@ -0,0 +1,31 @@ +package prettylog + +import ( + "log/slog" + "os" +) + +const ( + envLocal = "local" + envDev = "dev" + envProd = "prod" +) + +func SetupLogger(env string) *slog.Logger { + var log *slog.Logger + + switch env { + case envLocal: + log = slog.New(NewHandler(&slog.HandlerOptions{ + Level: slog.LevelInfo, + AddSource: false, + ReplaceAttr: nil, + })) + case envDev: + log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + case envProd: + log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + } + + return log +} From 688d167c2ed3a8e9f492a15a7d3e1553f4d4e334 Mon Sep 17 00:00:00 2001 From: DingoPortable <26508358+Sergeydigl3@users.noreply.github.com> Date: Sun, 30 Jun 2024 05:54:25 +0300 Subject: [PATCH 10/56] refactor: all code for searcher-agent refactored. Added conf parsing, rabbitmq consuming finished. --- .gitignore | 1 + cmd/searcher-agent/main.go | 83 ++++++++++++---- config/searcher-agent/local.yaml.example | 4 + go.mod | 1 + go.sum | 2 + internal/searcher-agent/app/app.go | 17 ++++ .../searcher-agent/app/rabbit/app_rabbit.go | 96 +++++++++++++++++++ internal/searcher-agent/config/config.go | 53 ++++++++++ 8 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 config/searcher-agent/local.yaml.example create mode 100644 internal/searcher-agent/app/app.go create mode 100644 internal/searcher-agent/app/rabbit/app_rabbit.go create mode 100644 internal/searcher-agent/config/config.go diff --git a/.gitignore b/.gitignore index eaabe9a..505d753 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ go.work.sum # env gitignore /config/searcher/local.yaml +/config/searcher-agent/local.yaml diff --git a/cmd/searcher-agent/main.go b/cmd/searcher-agent/main.go index 79fa52f..a62c270 100644 --- a/cmd/searcher-agent/main.go +++ b/cmd/searcher-agent/main.go @@ -1,44 +1,93 @@ -package searcher_agent_cmd +package main import ( "fmt" + "github.com/getz-devs/librakeeper-server/internal/searcher-agent/app" + "github.com/getz-devs/librakeeper-server/internal/searcher-agent/config" + "github.com/getz-devs/librakeeper-server/lib/prettylog" colly "github.com/gocolly/colly" + "log/slog" + "os" + "os/signal" + "syscall" ) func main() { - // TODO - err := scrapISBNFindBook("9785206000344") - if err != nil { - return - } + //err := scrapISBNFindBook("9785206000344") + //if err != nil { + // panic(err) + //} + + //const rabbitUrl = "amqp://guest:guest@192.168.1.161:5672/" + + cfg := config.MustLoad() + + log := prettylog.SetupLogger(cfg.Env) + + log.Info("starting ...", + slog.String("env", cfg.Env), + slog.Any("config", cfg), + ) + + application := app.New(cfg.ConnectUrl, cfg.QueueName, log) + go application.AppRabbit.MustRun() + + // --------------------------- Register stop signal --------------------------- + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + + // --------------------------- Wait for stop signal --------------------------- + sign := <-stop + + log.Info("shutting down ...", + slog.String("signal", sign.String()), + ) + + application.AppRabbit.Close() + + //application. + + log.Info("application fully stopped") } -var ( - const findbookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" -) +const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" + func scrapISBNFindBook(isbn string) error { + var books []*bookInShop + c := colly.NewCollector() c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" c.OnHTML( - "section.container section.results", + "section.container.results", func(e *colly.HTMLElement) { //fmt.Println(e.Attr("href")) - results_book := e.ChildText("") + //fmt.Println("Founded \n\n", e) + e.ForEach("div.row.results__line", func(_ int, e *colly.HTMLElement) { + book := &bookInShop{} + e.Unmarshal(book) + + books = append(books, book) + }) - // TODO }, ) - preparedUrl := fmt.Sprintf(findbookUrlTemplate, isbn) + preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) err := c.Visit(preparedUrl) if err != nil { return err } - - return "" + for _, p := range books { + fmt.Printf("%+v\n\n", p) + } + return nil } -type book_result struct { - // TODO +type bookInShop struct { + Title string `selector:"div.results__book-name > a"` + Author string `selector:"div.results__authors"` + Publishing string `selector:"div.results__publishing"` + ImgUrl string `selector:"a.results__image > img" attr:"src"` + ShopName string `selector:"div.results__shop-name > a"` } diff --git a/config/searcher-agent/local.yaml.example b/config/searcher-agent/local.yaml.example new file mode 100644 index 0000000..8a15a3c --- /dev/null +++ b/config/searcher-agent/local.yaml.example @@ -0,0 +1,4 @@ +env: "local" + +queue_name: "searcher" +connect_url: "amqp://guest:guest@192.168.1.161:5672/" \ No newline at end of file diff --git a/go.mod b/go.mod index 6253fa7..e0ddcd0 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/kennygrant/sanitize v1.2.4 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/temoto/robotstxt v1.1.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/go.sum b/go.sum index 68fceee..c6cad5b 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/searcher-agent/app/app.go b/internal/searcher-agent/app/app.go new file mode 100644 index 0000000..211ecfe --- /dev/null +++ b/internal/searcher-agent/app/app.go @@ -0,0 +1,17 @@ +package app + +import ( + app_rabbit "github.com/getz-devs/librakeeper-server/internal/searcher-agent/app/rabbit" + "log/slog" +) + +type App struct { + AppRabbit *app_rabbit.RabbitApp +} + +func New(rabbitUrl string, queueName string, log *slog.Logger) *App { + appRabbit := app_rabbit.New(rabbitUrl, queueName, log) + return &App{ + AppRabbit: appRabbit, + } +} diff --git a/internal/searcher-agent/app/rabbit/app_rabbit.go b/internal/searcher-agent/app/rabbit/app_rabbit.go new file mode 100644 index 0000000..794e5cc --- /dev/null +++ b/internal/searcher-agent/app/rabbit/app_rabbit.go @@ -0,0 +1,96 @@ +package app_rabbit + +import ( + "fmt" + amqp "github.com/rabbitmq/amqp091-go" + "log/slog" +) + +type RabbitApp struct { + connection *amqp.Connection + channel *amqp.Channel + msgs <-chan amqp.Delivery + log *slog.Logger +} + +// const rabbitUrl = "amqp://guest:guest@localhost:5672/" + +func failOnError(err error, msg string) { + if err != nil { + panic(fmt.Errorf("%s: %w", msg, err)) + } +} + +func New(rabbitUrl string, queueName string, log *slog.Logger) *RabbitApp { + const op = "rabbitmq.RabbitApp.New" + log = log.With(slog.String("op", op)) + + conn, err := amqp.Dial(rabbitUrl) + failOnError(err, "Failed to connect to RabbitMQ") + + ch, err := conn.Channel() + failOnError(err, "Failed to open a channel") + + q, err := ch.QueueDeclare( + queueName, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + failOnError(err, "Failed to declare a queue") + msgs, err := ch.Consume( + q.Name, // queue + "", // consumer + true, // auto-ack + false, // exclusive + false, // no-local + false, // no-wait + nil, // args + ) + failOnError(err, "Failed to register a consumer") + log = log.With("queue", q.Name) + + log.Info("Connected to RabbitMQ") + return &RabbitApp{ + connection: conn, + channel: ch, + log: log, + msgs: msgs, + } +} + +func (r *RabbitApp) Close() { + r.channel.Close() + r.connection.Close() +} + +func (r *RabbitApp) Run() error { + const op = "rabbitmq.RabbitApp.Run" + log := r.log.With(slog.String("op", op)) + log.Info(" [*] Waiting for messages. To exit press CTRL+C") + for d := range r.msgs { + logger := r.log.With(slog.String("messageID", d.MessageId)) + logger.Info("Received a message") + if err := processMessage(d, logger); err != nil { + return err + } + } + return nil +} + +func processMessage(d amqp.Delivery, log *slog.Logger) error { + const op = "rabbitmq.processMessage" + log = log.With(slog.String("op", op)) + log.Info("Processing message", + slog.String("messageID", d.MessageId), + slog.Any("body", d.Body)) + return nil +} + +func (r *RabbitApp) MustRun() { + if err := r.Run(); err != nil { + panic(err) + } +} diff --git a/internal/searcher-agent/config/config.go b/internal/searcher-agent/config/config.go new file mode 100644 index 0000000..de13140 --- /dev/null +++ b/internal/searcher-agent/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "flag" + "github.com/ilyakaznacheev/cleanenv" + "os" +) + +type Config struct { + Env string `yaml:"env" env-default:"local"` + + QueueName string `yaml:"queue_name" env:"QUEUE_NAME" env-default:"searcher"` + ConnectUrl string `yaml:"connect_url" env:"CONNECT_URL" env-required:"true"` +} + +func MustLoad() *Config { + path := fetchConfigPath() + if path == "" { + panic("config path is empty") + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + panic("config file doesn't exist: " + path) + } + + var cfg Config + + err := cleanenv.ReadConfig(path, &cfg) + if err != nil { + panic("failed to read config: " + err.Error()) + } + + return &cfg +} + +// fetchConfigPath fetches config path from command line flag or environment variable +// Priority: command line flag > environment variable > default +func fetchConfigPath() string { + var res string + + flag.StringVar(&res, "config", "", "path to config file") + flag.Parse() + + if res == "" { + res = os.Getenv("CONFIG_PATH") + } + + //if res == "" { + // res = "config.yml" + //} + + return res +} From bde96ec1d4aba60f82a1fad336a9ba7f221f05c6 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:34:23 +0300 Subject: [PATCH 11/56] rabbitmq features complete --- cmd/searcher-agent/main.go | 44 --------- cmd/searcher/main.go | 8 +- config/searcher/local.yaml.example | 6 +- .../searcher-agent/app/rabbit/app_rabbit.go | 18 ++-- internal/searcher-agent/rabbit/rabbit.go | 76 ++++++++++++++ .../domain/bookModels/books.go | 53 ++++++++++ internal/searcher/app/app.go | 5 +- internal/searcher/config/config.go | 7 ++ internal/searcher/domain/models/books.go | 11 --- internal/searcher/grpc/server.go | 6 +- .../searcher/rabbitProvider/rabbitProvider.go | 98 ++++++++++++++++++ .../searcher/services/searcher/searcher.go | 48 ++++++--- internal/searcher/storage/mongo/mongo.go | 99 +++++++++++-------- lib/rabbit/rabbit.proto | 9 ++ 14 files changed, 360 insertions(+), 128 deletions(-) create mode 100644 internal/searcher-agent/rabbit/rabbit.go create mode 100644 internal/searcher-shared/domain/bookModels/books.go delete mode 100644 internal/searcher/domain/models/books.go create mode 100644 internal/searcher/rabbitProvider/rabbitProvider.go create mode 100644 lib/rabbit/rabbit.proto diff --git a/cmd/searcher-agent/main.go b/cmd/searcher-agent/main.go index a62c270..91ce0cf 100644 --- a/cmd/searcher-agent/main.go +++ b/cmd/searcher-agent/main.go @@ -1,11 +1,9 @@ package main import ( - "fmt" "github.com/getz-devs/librakeeper-server/internal/searcher-agent/app" "github.com/getz-devs/librakeeper-server/internal/searcher-agent/config" "github.com/getz-devs/librakeeper-server/lib/prettylog" - colly "github.com/gocolly/colly" "log/slog" "os" "os/signal" @@ -49,45 +47,3 @@ func main() { log.Info("application fully stopped") } - -const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" - -func scrapISBNFindBook(isbn string) error { - var books []*bookInShop - - c := colly.NewCollector() - c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" - c.OnHTML( - "section.container.results", - func(e *colly.HTMLElement) { - //fmt.Println(e.Attr("href")) - //fmt.Println("Founded \n\n", e) - e.ForEach("div.row.results__line", func(_ int, e *colly.HTMLElement) { - book := &bookInShop{} - e.Unmarshal(book) - - books = append(books, book) - }) - - }, - ) - - preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) - err := c.Visit(preparedUrl) - if err != nil { - return err - } - - for _, p := range books { - fmt.Printf("%+v\n\n", p) - } - return nil -} - -type bookInShop struct { - Title string `selector:"div.results__book-name > a"` - Author string `selector:"div.results__authors"` - Publishing string `selector:"div.results__publishing"` - ImgUrl string `selector:"a.results__image > img" attr:"src"` - ShopName string `selector:"div.results__shop-name > a"` -} diff --git a/cmd/searcher/main.go b/cmd/searcher/main.go index cba6f60..ff1aa77 100644 --- a/cmd/searcher/main.go +++ b/cmd/searcher/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/getz-devs/librakeeper-server/internal/searcher/app" "github.com/getz-devs/librakeeper-server/internal/searcher/config" + "github.com/getz-devs/librakeeper-server/internal/searcher/rabbitProvider" mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher/storage/mongo" "github.com/getz-devs/librakeeper-server/lib/prettylog" "log/slog" @@ -28,8 +29,13 @@ func main() { Collection: cfg.DatabaseMongo.CollectionName, } + rabbitConfig := rabbitProvider.RabbitConfig{ + RabbitUrl: cfg.Rabbit.URL, + QueueName: cfg.Rabbit.QueueName, + } + // --------------------------- Start Application server ----------------------- - application := app.New(log, cfg.GRPC.Port, databaseMongoConfig) + application := app.New(log, cfg.GRPC.Port, databaseMongoConfig, rabbitConfig) go application.GRPCSrv.MustRun() // --------------------------- Register stop signal --------------------------- diff --git a/config/searcher/local.yaml.example b/config/searcher/local.yaml.example index 974bfb4..33f0efd 100644 --- a/config/searcher/local.yaml.example +++ b/config/searcher/local.yaml.example @@ -8,4 +8,8 @@ grpc: database_mongo: connect_url: "mongodb://192.168.1.199:27017/" database_name: "TempData" - collection_name_books: "books" \ No newline at end of file + collection_name_books: "books" + +rabbit: + url: "amqp://guest:guest@192.168.1.161:5672/" + queue_name: "searcher" \ No newline at end of file diff --git a/internal/searcher-agent/app/rabbit/app_rabbit.go b/internal/searcher-agent/app/rabbit/app_rabbit.go index 794e5cc..3ad3537 100644 --- a/internal/searcher-agent/app/rabbit/app_rabbit.go +++ b/internal/searcher-agent/app/rabbit/app_rabbit.go @@ -2,6 +2,7 @@ package app_rabbit import ( "fmt" + "github.com/getz-devs/librakeeper-server/internal/searcher-agent/rabbit" amqp "github.com/rabbitmq/amqp091-go" "log/slog" ) @@ -73,22 +74,19 @@ func (r *RabbitApp) Run() error { for d := range r.msgs { logger := r.log.With(slog.String("messageID", d.MessageId)) logger.Info("Received a message") - if err := processMessage(d, logger); err != nil { + if err := rabbit.Handler(d, logger); err != nil { + + logger.Error("Failed to parse message", err) + err := d.Nack(false, false) + if err != nil { + log.Error("Failed to nack message", err) + } return err } } return nil } -func processMessage(d amqp.Delivery, log *slog.Logger) error { - const op = "rabbitmq.processMessage" - log = log.With(slog.String("op", op)) - log.Info("Processing message", - slog.String("messageID", d.MessageId), - slog.Any("body", d.Body)) - return nil -} - func (r *RabbitApp) MustRun() { if err := r.Run(); err != nil { panic(err) diff --git a/internal/searcher-agent/rabbit/rabbit.go b/internal/searcher-agent/rabbit/rabbit.go new file mode 100644 index 0000000..474152a --- /dev/null +++ b/internal/searcher-agent/rabbit/rabbit.go @@ -0,0 +1,76 @@ +package rabbit + +import ( + "fmt" + "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" + rabbitDefines "github.com/getz-devs/librakeeper-server/lib/rabbit/getz.rabbitProto.v1" + "github.com/gocolly/colly" + "github.com/golang/protobuf/proto" + amqp "github.com/rabbitmq/amqp091-go" + "log/slog" +) + +//type Parser struct { +// log *slog.Logger +//} +// +//func New(log *slog.Logger) *Parser { +// const op +// return &Parser{ +// log: log, +// } +//} + +func Handler(delivery amqp.Delivery, log *slog.Logger) error { + const op = "rabbit.Handler" + log = log.With(slog.String("op", op)) + + msg := &rabbitDefines.ISBNMessage{} + err := proto.Unmarshal(delivery.Body, msg) + if err != nil { + return err + } + + err = scrapISBNFindBook(msg.GetIsbn()) + if err != nil { + return err + } + + return nil +} + +const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" + +func scrapISBNFindBook(isbn string) error { + var books []*bookModels.BookInShop + + c := colly.NewCollector() + c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (HTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" + c.OnHTML( + "section.container.results", + func(e *colly.HTMLElement) { + + e.ForEach("div.row.results__line", func(_ int, e *colly.HTMLElement) { + book := &bookModels.BookInShop{} + err := e.Unmarshal(book) + if err != nil { + return + } + + books = append(books, book) + }) + + }, + ) + + preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) + err := c.Visit(preparedUrl) + if err != nil { + return err + } + + for _, p := range books { + fmt.Printf("%+v\n\n", p) + } + return nil +} diff --git a/internal/searcher-shared/domain/bookModels/books.go b/internal/searcher-shared/domain/bookModels/books.go new file mode 100644 index 0000000..b383745 --- /dev/null +++ b/internal/searcher-shared/domain/bookModels/books.go @@ -0,0 +1,53 @@ +package bookModels + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +type BookInShop struct { + Title string `selector:"div.results__book-name > a" bson:"title,omitempty"` + Author string `selector:"div.results__authors" bson:"author,omitempty"` + Publishing string `selector:"div.results__publishing" bson:"publishing,omitempty"` + ImgUrl string `selector:"a.results__image > img" attr:"src" bson:"img_url"` + ShopName string `selector:"div.results__shop-name > a" bson:"shop_name"` +} + +type RequestStatus int + +// Pending, Success, Failed +const ( + Pending RequestStatus = iota + Success + Failed +) + +type SearchRequest struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Isbn string `bson:"isbn"` + Status RequestStatus `bson:"status,omitempty"` + Books []*BookInShop `bson:"books,omitempty"` + CreatedAt primitive.DateTime `bson:"created_at,omitempty"` + UpdatedAt primitive.DateTime `bson:"updated_at,omitempty"` +} + +// New creates a new SearchRequest with the provided ISBN, current time, and initial values. +// +// Parameters: +// +// isbn - the ISBN of the book +// +// Returns: +// +// SearchRequest - the newly created SearchRequest +func New(isbn string) SearchRequest { + currentTime := primitive.NewDateTimeFromTime(time.Now()) + return SearchRequest{ + ID: primitive.NewObjectID(), + Isbn: isbn, + CreatedAt: currentTime, + UpdatedAt: currentTime, + Status: Pending, + Books: []*BookInShop{}, + } +} diff --git a/internal/searcher/app/app.go b/internal/searcher/app/app.go index 499a6d6..cff20b5 100644 --- a/internal/searcher/app/app.go +++ b/internal/searcher/app/app.go @@ -2,6 +2,7 @@ package app import ( grpcapp "github.com/getz-devs/librakeeper-server/internal/searcher/app/grpc" + "github.com/getz-devs/librakeeper-server/internal/searcher/rabbitProvider" "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher/storage/mongo" "log/slog" @@ -16,9 +17,11 @@ func New( log *slog.Logger, grpcPort int, databaseMongoConfig mongostorage.DatabaseMongoConfig, + rabbitConfig rabbitProvider.RabbitConfig, ) *App { storage := mongostorage.New(databaseMongoConfig) - searcherService := searcher_service.New(log, storage) + rabbit := rabbitProvider.New(rabbitConfig, log) + searcherService := searcher_service.New(log, storage, rabbit) grpcApp := grpcapp.New(log, searcherService, grpcPort) return &App{ diff --git a/internal/searcher/config/config.go b/internal/searcher/config/config.go index ce3a20f..9f4a731 100644 --- a/internal/searcher/config/config.go +++ b/internal/searcher/config/config.go @@ -15,6 +15,8 @@ type Config struct { GRPC GRPCConfig `yaml:"grpc"` DatabaseMongo DatabaseMongoConfig `yaml:"database_mongo"` + + Rabbit RabbitConfig `yaml:"rabbit"` } type GRPCConfig struct { @@ -28,6 +30,11 @@ type DatabaseMongoConfig struct { CollectionName string `yaml:"collection_name_books" env-required:"true"` } +type RabbitConfig struct { + URL string `yaml:"url" env-required:"true"` + QueueName string `yaml:"queue_name" env-required:"true"` +} + func MustLoad() *Config { path := fetchConfigPath() if path == "" { diff --git a/internal/searcher/domain/models/books.go b/internal/searcher/domain/models/books.go deleted file mode 100644 index 7628200..0000000 --- a/internal/searcher/domain/models/books.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -type BookSearchResult struct { - ISBN string - Title string - Author string - Publisher string - Year string -} - -type BooksSearchResult []*BookSearchResult diff --git a/internal/searcher/grpc/server.go b/internal/searcher/grpc/server.go index 0b26064..2083751 100644 --- a/internal/searcher/grpc/server.go +++ b/internal/searcher/grpc/server.go @@ -3,7 +3,7 @@ package searcher import ( "context" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" - searcher_service "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" + searcherservice "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -12,11 +12,11 @@ import ( type serverAPI struct { searcherv1.UnimplementedSearcherServer - searcherService *searcher_service.SearcherService + searcherService *searcherservice.SearcherService log *slog.Logger } -func Register(gRPC *grpc.Server, searcherService *searcher_service.SearcherService, log *slog.Logger) { +func Register(gRPC *grpc.Server, searcherService *searcherservice.SearcherService, log *slog.Logger) { searcherv1.RegisterSearcherServer(gRPC, &serverAPI{searcherService: searcherService, log: log}) } diff --git a/internal/searcher/rabbitProvider/rabbitProvider.go b/internal/searcher/rabbitProvider/rabbitProvider.go new file mode 100644 index 0000000..c910a98 --- /dev/null +++ b/internal/searcher/rabbitProvider/rabbitProvider.go @@ -0,0 +1,98 @@ +package rabbitProvider + +import ( + "context" + "fmt" + rabbitDefines "github.com/getz-devs/librakeeper-server/lib/rabbit/getz.rabbitProto.v1" + amqp "github.com/rabbitmq/amqp091-go" + "google.golang.org/protobuf/proto" + "log/slog" +) + +type RabbitConfig struct { + RabbitUrl string + QueueName string +} + +type RabbitService struct { + log *slog.Logger + ch *amqp.Channel + q amqp.Queue +} + +func New(rabbitConfig RabbitConfig, log *slog.Logger) *RabbitService { + const op = "rabbitProvider.New" + log = log.With(slog.String("op", op)) + + conn, err := amqp.Dial(rabbitConfig.RabbitUrl) + failOnError(err, "Failed to connect to RabbitMQ") + + ch, err := conn.Channel() + failOnError(err, "Failed to open a channel") + + q, err := ch.QueueDeclare( + rabbitConfig.QueueName, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + failOnError(err, "Failed to declare a queue") + + return &RabbitService{ + log: log, + ch: ch, + q: q, + } +} + +func (s *RabbitService) sendMessage(ctx context.Context, message []byte) error { + const op = "rabbitProvider.RabbitService.SendMessage" + s.log.With(slog.String("op", op)) + err := s.ch.PublishWithContext( + ctx, + "", // exchange + s.q.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: message, + }) + if err != nil { + s.log.Error(err.Error()) + return fmt.Errorf("%s: %w", op, err) + } + + s.log.Info(" [x] Sent ", slog.String("message", string(message))) + + return nil +} +func (s *RabbitService) AddRequest(ctx context.Context, isbn string) error { + protoMessage := &rabbitDefines.ISBNMessage{ + Isbn: isbn, + } + out, err := proto.Marshal(protoMessage) + if err != nil { + s.log.Error(err.Error()) + return err + } + return s.sendMessage(ctx, out) +} + +func (s *RabbitService) Close() { + const op = "rabbitProvider.RabbitService.Close" + s.log.With(slog.String("op", op)) + err := s.ch.Close() + if err != nil { + s.log.Error(err.Error()) + return + } +} + +func failOnError(err error, msg string) { + if err != nil { + panic(fmt.Errorf("%s: %w", msg, err)) + } +} diff --git a/internal/searcher/services/searcher/searcher.go b/internal/searcher/services/searcher/searcher.go index f6561c5..279d009 100644 --- a/internal/searcher/services/searcher/searcher.go +++ b/internal/searcher/services/searcher/searcher.go @@ -2,29 +2,33 @@ package searcher_service import ( "context" - "fmt" - "github.com/getz-devs/librakeeper-server/internal/searcher/domain/models" + "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" "log/slog" ) +type RequestExecutor interface { + AddRequest(ctx context.Context, isbn string) error +} + type SearcherService struct { - log *slog.Logger - ISBNSearcher ISBNSearcher + log *slog.Logger + requestStorage RequestStorage + requestExecutor RequestExecutor } -// ISBNSearcher return models.BooksSearchResult -type ISBNSearcher interface { - SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) +type RequestStorage interface { + FindOrCreateRequest(ctx context.Context, isbn string) (bookModels.SearchRequest, bool, error) } -func New(log *slog.Logger, ISBNSearcher ISBNSearcher) *SearcherService { +func New(log *slog.Logger, requestStorage RequestStorage, requestExecutor RequestExecutor) *SearcherService { return &SearcherService{ - log: log, - ISBNSearcher: ISBNSearcher, + log: log, + requestStorage: requestStorage, + requestExecutor: requestExecutor, } } -func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { +func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (bookModels.SearchRequest, error) { const op = "searcher.SearcherService.SearchByISBN" s.log.With( slog.String("op", op), @@ -32,11 +36,27 @@ func (s *SearcherService) SearchByISBN(ctx context.Context, isbn string) (*model ) s.log.Info("searching by ISBN") - data, err := s.ISBNSearcher.SearchByISBN(ctx, isbn) + data, created, err := s.requestStorage.FindOrCreateRequest(ctx, isbn) if err != nil { - s.log.Error("error when searching by ISBN", err) - return nil, fmt.Errorf("%s ,error when searching by ISBN: %w", op, err) + return bookModels.SearchRequest{}, err + } + if created { + s.log.Info("request created") + err := s.requestExecutor.AddRequest(ctx, isbn) + if err != nil { + return bookModels.SearchRequest{}, err + } } return data, nil } + +//func (s *SearcherService) FindOrCreateRequest(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { +// const op = "searcher.SearcherService.FindOrCreateRequest" +// s.log.With( +// slog.String("op", op), +// slog.String("isbn", isbn), +// ) +// s.log.Info("searching by ISBN") +// return &models.BooksSearchResult{}, nil +//} diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go index 6d1fb1f..5a256a7 100644 --- a/internal/searcher/storage/mongo/mongo.go +++ b/internal/searcher/storage/mongo/mongo.go @@ -2,12 +2,10 @@ package mongostorage import ( "context" - "fmt" - "github.com/getz-devs/librakeeper-server/internal/searcher/domain/models" + "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "log" ) type Storage struct { @@ -22,8 +20,6 @@ type DatabaseMongoConfig struct { } func New(databaseConfig DatabaseMongoConfig) *Storage { - const op = "storage.mongo.New" - client, err := mongo.Connect(context.TODO(), options.Client(). ApplyURI(databaseConfig.ConnectUrl)) if err != nil { @@ -39,50 +35,67 @@ func New(databaseConfig DatabaseMongoConfig) *Storage { } func (s *Storage) Close() { - const op = "storage.mongo.Close" if err := s.client.Disconnect(context.TODO()); err != nil { panic(err) } } -func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { - const op = "storage.mongo.FindByISBN" - - // Pass these options to the Find method - findOptions := options.Find() - findOptions.SetLimit(5) - - // Here's an array in which you can store the decoded documents - var results models.BooksSearchResult - - // Passing bson.D{{}} as the filter matches all documents in the collection - cur, err := s.col.Find(ctx, bson.D{{}}, findOptions) +//func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { +// const op = "storage.mongo.FindByISBN" +// +// // Pass these options to the Find method +// findOptions := options.Find() +// findOptions.SetLimit(5) +// +// // Here's an array in which you can store the decoded documents +// var results models.BooksSearchResult +// +// // Passing bson.D{{}} as the filter matches all documents in the collection +// cur, err := s.col.Find(ctx, bson.D{{}}, findOptions) +// if err != nil { +// log.Fatal(err) +// } +// +// // Finding multiple documents returns a cursor +// // Iterating through the cursor allows us to decode documents one at a time +// for cur.Next(ctx) { +// +// // create a value into which the single document can be decoded +// var elem models.BookSearchResult +// err := cur.Decode(&elem) +// if err != nil { +// log.Fatal(err) +// } +// +// results = append(results, &elem) +// } +// +// if err := cur.Err(); err != nil { +// return nil, fmt.Errorf("%s ,error when reading cursor: %w", op, err) +// } +// +// // Close the cursor once finished +// if err := cur.Close(ctx); err != nil { +// return nil, fmt.Errorf("%s ,error when closing cursor: %w", op, err) +// } +// +// return &results, nil +//} + +func (s *Storage) FindOrCreateRequest(ctx context.Context, isbn string) (bookModels.SearchRequest, bool, error) { + // insert if not exist (upsert) + filter := bson.D{{"isbn", isbn}} + insertValue := bookModels.New(isbn) + value := bson.D{{"$setOnInsert", insertValue}} + opts := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After) + + var result bookModels.SearchRequest + err := s.col.FindOneAndUpdate(ctx, filter, value, opts).Decode(&result) if err != nil { - log.Fatal(err) - } - - // Finding multiple documents returns a cursor - // Iterating through the cursor allows us to decode documents one at a time - for cur.Next(ctx) { - - // create a value into which the single document can be decoded - var elem models.BookSearchResult - err := cur.Decode(&elem) - if err != nil { - log.Fatal(err) - } - - results = append(results, &elem) - } - - if err := cur.Err(); err != nil { - return nil, fmt.Errorf("%s ,error when reading cursor: %w", op, err) + return bookModels.SearchRequest{}, false, err } - - // Close the cursor once finished - if err := cur.Close(ctx); err != nil { - return nil, fmt.Errorf("%s ,error when closing cursor: %w", op, err) + if insertValue.ID == result.ID { + return bookModels.SearchRequest{}, true, nil } - - return &results, nil + return result, false, nil } diff --git a/lib/rabbit/rabbit.proto b/lib/rabbit/rabbit.proto new file mode 100644 index 0000000..1100708 --- /dev/null +++ b/lib/rabbit/rabbit.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package pb; + +option go_package = "getz.rabbitProto.v1;rabbitDefines"; + +message ISBNMessage { + string isbn = 1; +} \ No newline at end of file From 7d1b15d9dfb6a79b718c6090d30890e6e17eb03c Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 17:56:39 +0300 Subject: [PATCH 12/56] add mongo and basic models --- go.mod | 8 ++++ internal/server/config/config.go | 59 ++++++++++++----------------- internal/server/models/book.go | 18 +++++++++ internal/server/models/bookshelf.go | 15 ++++++++ internal/server/models/user.go | 23 +++++++++++ internal/server/storage/mongo.go | 41 ++++++++++++++++++++ 6 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 internal/server/models/book.go create mode 100644 internal/server/models/bookshelf.go create mode 100644 internal/server/models/user.go create mode 100644 internal/server/storage/mongo.go diff --git a/go.mod b/go.mod index 0b22760..48329ce 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/goccy/go-json v0.10.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect @@ -37,14 +38,21 @@ require ( github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.mongodb.org/mongo-driver v1.16.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect diff --git a/internal/server/config/config.go b/internal/server/config/config.go index 7a44442..e74ead5 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -2,62 +2,51 @@ package config import ( "flag" - "github.com/ilyakaznacheev/cleanenv" + "fmt" "os" - "time" + + "github.com/ilyakaznacheev/cleanenv" ) +// Config represents the application's configuration. type Config struct { - Env string `yaml:"env" env-default:"local"` - StoragePath string `yaml:"storage_path" env-required:"true"` - SearchConfig SearchConfig `yaml:"search_config"` - ServerConfig ServerConfig `yaml:"server_config"` -} - -type SearchConfig struct { - Port int `yaml:"port" env-default:"44044"` - Timeout time.Duration `yaml:"timeout" env-default:"10h"` -} - -type ServerConfig struct { - Port int `yaml:"port" env-default:"8080"` + Env string `yaml:"env" env-default:"local"` + MongoURI string `yaml:"mongo_uri" env-required:"true"` + Database string `yaml:"database" env-required:"true"` + Server struct { + Port int `yaml:"port" env-default:"8080"` + } `yaml:"server"` } +// MustLoad loads the configuration from the specified path and environment variables. func MustLoad() *Config { - path := fetchConfigPath() - if path == "" { + configPath := fetchConfigPath() + if configPath == "" { panic("config path is empty") } - if _, err := os.Stat(path); os.IsNotExist(err) { - panic("config file doesn't exist: " + path) + if _, err := os.Stat(configPath); os.IsNotExist(err) { + panic("config file doesn't exist: " + configPath) } var cfg Config - - err := cleanenv.ReadConfig(path, &cfg) - if err != nil { - panic("failed to read config: " + err.Error()) + if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { + panic(fmt.Errorf("failed to read config: %w", err)) } return &cfg } -// fetchConfigPath fetches config path from command line flag or environment variable -// Priority: command line flag > environment variable > default +// fetchConfigPath fetches the config path from command-line flags or environment variables. +// Priority: command-line flag > environment variable. func fetchConfigPath() string { - var res string - - flag.StringVar(&res, "config", "", "path to config file") + var configPath string + flag.StringVar(&configPath, "config", "", "Path to the config file") flag.Parse() - if res == "" { - res = os.Getenv("CONFIG_PATH") + if configPath == "" { + configPath = os.Getenv("CONFIG_PATH") } - //if res == "" { - // res = "config.yml" - //} - - return res + return configPath } diff --git a/internal/server/models/book.go b/internal/server/models/book.go new file mode 100644 index 0000000..7eb88f6 --- /dev/null +++ b/internal/server/models/book.go @@ -0,0 +1,18 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// Book represents a book in the library. +type Book struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + Title string `bson:"title" json:"title"` + Author string `bson:"author" json:"author"` + ISBN string `bson:"isbn" json:"isbn"` + Description string `bson:"description" json:"description"` + CoverImage string `bson:"cover_image" json:"cover_image"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} diff --git a/internal/server/models/bookshelf.go b/internal/server/models/bookshelf.go new file mode 100644 index 0000000..67870df --- /dev/null +++ b/internal/server/models/bookshelf.go @@ -0,0 +1,15 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "time" +) + +// Bookshelf represents a collection of books. +type Bookshelf struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + UserID primitive.ObjectID `bson:"user_id" json:"user_id"` + Name string `bson:"name" json:"name"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} diff --git a/internal/server/models/user.go b/internal/server/models/user.go new file mode 100644 index 0000000..e593316 --- /dev/null +++ b/internal/server/models/user.go @@ -0,0 +1,23 @@ +package models + +import ( + "go.mongodb.org/mongo-driver/bson" + "time" +) + +// User represents a user in the system. +type User struct { + ID string `bson:"_id" json:"id"` // Firebase UID as primary key + DisplayName string `bson:"display_name" json:"display_name"` + // Add other fields as needed + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` +} + +func (u *User) ToMap() bson.M { + return bson.M{ + "display_name": u.DisplayName, + "created_at": u.CreatedAt, + "updated_at": u.UpdatedAt, + } +} diff --git a/internal/server/storage/mongo.go b/internal/server/storage/mongo.go new file mode 100644 index 0000000..e21a0e0 --- /dev/null +++ b/internal/server/storage/mongo.go @@ -0,0 +1,41 @@ +package database + +import ( + "context" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/config" + "log/slog" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + _db *mongo.Database + _log *slog.Logger +) + +func Initialize(cfg *config.Config, log *slog.Logger) *mongo.Database { + _log = log + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.MongoURI)) + if err != nil { + _log.Error("failed to connect to mongodb", slog.Any("error", err)) + panic(err) + } + + _db = client.Database(cfg.Database) + _log.Info("connected to mongodb", slog.String("database", cfg.Database)) + return _db +} + +func GetCollection(name string) *mongo.Collection { + if _db == nil { + panic(fmt.Errorf("mongodb has not been initialized")) + } + + return _db.Collection(name) +} From 13f11ca45bec224715ed811494164a1c6c04ee72 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 18:32:59 +0300 Subject: [PATCH 13/56] created services for CRUD ops --- internal/server/services/books.go | 133 ++++++++++++++++++++++++ internal/server/services/bookshelves.go | 106 +++++++++++++++++++ internal/server/services/users.go | 83 +++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 internal/server/services/books.go create mode 100644 internal/server/services/bookshelves.go create mode 100644 internal/server/services/users.go diff --git a/internal/server/services/books.go b/internal/server/services/books.go new file mode 100644 index 0000000..f90981e --- /dev/null +++ b/internal/server/services/books.go @@ -0,0 +1,133 @@ +package services + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log/slog" + "time" +) + +var ( + ErrBookNotFound = errors.New("book not found") +) + +type BookService struct { + collection *mongo.Collection + log *slog.Logger +} + +func NewBookService(collection *mongo.Collection, log *slog.Logger) *BookService { + return &BookService{ + collection: collection, + log: log, + } +} + +func (s *BookService) CreateBook(ctx context.Context, book *models.Book) (*models.Book, error) { + book.CreatedAt = time.Now() + book.UpdatedAt = time.Now() + + res, err := s.collection.InsertOne(ctx, book) + if err != nil { + s.log.Error("failed to create book", slog.Any("error", err)) + return nil, fmt.Errorf("failed to create book: %w", err) + } + + book.ID = res.InsertedID.(primitive.ObjectID) + return book, nil +} + +func (s *BookService) GetBook(ctx context.Context, bookID primitive.ObjectID) (*models.Book, error) { + var book models.Book + err := s.collection.FindOne(ctx, bson.M{"_id": bookID}).Decode(&book) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrBookNotFound + } + return nil, fmt.Errorf("failed to get book: %w", err) + } + return &book, nil +} + +func (s *BookService) GetBooks(ctx context.Context, page int64, limit int64) ([]*models.Book, error) { + findOptions := options.Find() + findOptions.SetSkip((page - 1) * limit) + findOptions.SetLimit(limit) + + cursor, err := s.collection.Find(ctx, bson.M{}, findOptions) + if err != nil { + s.log.Error("failed to get books", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get books: %w", err) + } + defer cursor.Close(ctx) + + var books []*models.Book + for cursor.Next(ctx) { + var book models.Book + if err := cursor.Decode(&book); err != nil { + return nil, fmt.Errorf("failed to decode book: %w", err) + } + books = append(books, &book) + } + + if err := cursor.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return books, nil +} + +func (s *BookService) GetBooksByBookshelfID(ctx context.Context, bookshelfID primitive.ObjectID, page int64, limit int64) ([]*models.Book, error) { + matchStage := bson.D{{"$match", bson.D{{"bookshelf_id", bookshelfID}}}} + skipStage := bson.D{{"$skip", (page - 1) * limit}} + limitStage := bson.D{{"$limit", limit}} + + cursor, err := s.collection.Aggregate(ctx, mongo.Pipeline{matchStage, skipStage, limitStage}) + if err != nil { + s.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get books by bookshelf id: %w", err) + } + defer cursor.Close(ctx) + + var books []*models.Book + for cursor.Next(ctx) { + var book models.Book + if err := cursor.Decode(&book); err != nil { + return nil, fmt.Errorf("failed to decode book: %w", err) + } + books = append(books, &book) + } + + if err := cursor.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return books, nil +} + +func (s *BookService) UpdateBook(ctx context.Context, bookID primitive.ObjectID, update bson.M) error { + update["updated_at"] = time.Now() + _, err := s.collection.UpdateOne(ctx, bson.M{"_id": bookID}, bson.M{"$set": update}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookNotFound + } + return fmt.Errorf("failed to update book: %w", err) + } + return nil +} + +func (s *BookService) DeleteBook(ctx context.Context, bookID primitive.ObjectID) error { + _, err := s.collection.DeleteOne(ctx, bson.M{"_id": bookID}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookNotFound + } + return fmt.Errorf("failed to delete book: %w", err) + } + return nil +} diff --git a/internal/server/services/bookshelves.go b/internal/server/services/bookshelves.go new file mode 100644 index 0000000..62ee5a8 --- /dev/null +++ b/internal/server/services/bookshelves.go @@ -0,0 +1,106 @@ +package services + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log/slog" + "time" +) + +var ( + ErrBookshelfNotFound = errors.New("bookshelf not found") +) + +type BookshelfService struct { + collection *mongo.Collection + log *slog.Logger +} + +func NewBookshelfService(collection *mongo.Collection, log *slog.Logger) *BookshelfService { + return &BookshelfService{ + collection: collection, + log: log, + } +} + +func (s *BookshelfService) CreateBookshelf(ctx context.Context, bookshelf *models.Bookshelf) (*models.Bookshelf, error) { + bookshelf.CreatedAt = time.Now() + bookshelf.UpdatedAt = time.Now() + + res, err := s.collection.InsertOne(ctx, bookshelf) + if err != nil { + s.log.Error("failed to create bookshelf", slog.Any("error", err)) + return nil, fmt.Errorf("failed to create bookshelf: %w", err) + } + + bookshelf.ID = res.InsertedID.(primitive.ObjectID) + return bookshelf, nil +} + +func (s *BookshelfService) GetBookshelf(ctx context.Context, bookshelfID primitive.ObjectID) (*models.Bookshelf, error) { + var bookshelf models.Bookshelf + err := s.collection.FindOne(ctx, bson.M{"_id": bookshelfID}).Decode(&bookshelf) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrBookshelfNotFound + } + return nil, fmt.Errorf("failed to get bookshelf: %w", err) + } + return &bookshelf, nil +} + +func (s *BookshelfService) GetBookshelvesByUserID(ctx context.Context, userID primitive.ObjectID, page int64, limit int64) ([]*models.Bookshelf, error) { + findOptions := options.Find() + findOptions.SetSkip((page - 1) * limit) + findOptions.SetLimit(limit) + + cursor, err := s.collection.Find(ctx, bson.M{"user_id": userID}, findOptions) + if err != nil { + s.log.Error("failed to get bookshelves", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get bookshelves: %w", err) + } + defer cursor.Close(ctx) + + var bookshelves []*models.Bookshelf + for cursor.Next(ctx) { + var bookshelf models.Bookshelf + if err := cursor.Decode(&bookshelf); err != nil { + return nil, fmt.Errorf("failed to decode bookshelf: %w", err) + } + bookshelves = append(bookshelves, &bookshelf) + } + + if err := cursor.Err(); err != nil { + return nil, fmt.Errorf("cursor error: %w", err) + } + return bookshelves, nil +} + +func (s *BookshelfService) UpdateBookshelf(ctx context.Context, bookshelfID primitive.ObjectID, update bson.M) error { + update["updated_at"] = time.Now() + _, err := s.collection.UpdateOne(ctx, bson.M{"_id": bookshelfID}, bson.M{"$set": update}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to update bookshelf: %w", err) + } + return nil +} + +func (s *BookshelfService) DeleteBookshelf(ctx context.Context, bookshelfID primitive.ObjectID) error { + _, err := s.collection.DeleteOne(ctx, bson.M{"_id": bookshelfID}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to delete bookshelf: %w", err) + } + return nil +} diff --git a/internal/server/services/users.go b/internal/server/services/users.go new file mode 100644 index 0000000..29bb339 --- /dev/null +++ b/internal/server/services/users.go @@ -0,0 +1,83 @@ +package services + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "log/slog" + "time" +) + +var ( + ErrUserAlreadyExists = errors.New("user already exists") + ErrUserNotFound = errors.New("user not found") +) + +type UserService struct { + collection *mongo.Collection + log *slog.Logger +} + +func NewUserService(collection *mongo.Collection, log *slog.Logger) *UserService { + return &UserService{ + collection: collection, + log: log, + } +} + +func (s *UserService) CreateUser(ctx context.Context, user *models.User) (*models.User, error) { + user.CreatedAt = time.Now() + user.UpdatedAt = time.Now() + + _, err := s.collection.InsertOne(ctx, user) + if err != nil { + // Check for duplicate key error (Firebase UID uniqueness) + var mongoErr mongo.WriteException + if errors.As(err, &mongoErr) && mongoErr.WriteErrors[0].Code == 11000 { + return nil, ErrUserAlreadyExists + } + + s.log.Error("failed to create user", slog.Any("error", err)) + return nil, fmt.Errorf("failed to create user: %w", err) + } + + return user, nil +} + +func (s *UserService) GetUserByID(ctx context.Context, userID string) (*models.User, error) { + var user models.User + err := s.collection.FindOne(ctx, bson.M{"_id": userID}).Decode(&user) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("failed to get user by id: %w", err) + } + return &user, nil +} + +func (s *UserService) UpdateUser(ctx context.Context, userID string, update bson.M) error { + update["updated_at"] = time.Now() + _, err := s.collection.UpdateOne(ctx, bson.M{"_id": userID}, bson.M{"$set": update}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrUserNotFound + } + return fmt.Errorf("failed to update user: %w", err) + } + return nil +} + +func (s *UserService) DeleteUser(ctx context.Context, userID string) error { + _, err := s.collection.DeleteOne(ctx, bson.M{"_id": userID}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrUserNotFound + } + return fmt.Errorf("failed to delete user: %w", err) + } + return nil +} From f48f27e3319306325be93a8591e91441c4ccd0f3 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 19:10:02 +0300 Subject: [PATCH 14/56] create handlers for models --- internal/server/handlers/books.go | 182 ++++++++++++++++++++++++ internal/server/handlers/bookshelves.go | 167 ++++++++++++++++++++++ internal/server/handlers/users.go | 112 +++++++++++++++ internal/server/models/book.go | 16 +++ internal/server/models/bookshelf.go | 13 +- internal/server/models/user.go | 9 +- 6 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 internal/server/handlers/books.go create mode 100644 internal/server/handlers/bookshelves.go create mode 100644 internal/server/handlers/users.go diff --git a/internal/server/handlers/books.go b/internal/server/handlers/books.go new file mode 100644 index 0000000..6d67032 --- /dev/null +++ b/internal/server/handlers/books.go @@ -0,0 +1,182 @@ +package handlers + +import ( + "errors" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" + "log/slog" + "net/http" + "strconv" +) + +type BookHandlers struct { + service *services.BookService + log *slog.Logger +} + +func NewBookHandlers(service *services.BookService, log *slog.Logger) *BookHandlers { + return &BookHandlers{ + service: service, + log: log, + } +} + +func (h *BookHandlers) CreateBook(c *gin.Context) { + var book models.Book + if err := c.BindJSON(&book); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx := c.Request.Context() + createdBook, err := h.service.CreateBook(ctx, &book) + if err != nil { + h.log.Error("failed to create book", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) + return + } + + c.JSON(http.StatusCreated, createdBook) +} + +func (h *BookHandlers) GetBook(c *gin.Context) { + bookIDHex := c.Param("id") + + bookID, err := primitive.ObjectIDFromHex(bookIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) + return + } + + ctx := c.Request.Context() + book, err := h.service.GetBook(ctx, bookID) + if err != nil { + if errors.Is(err, services.ErrBookNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to get book", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get book"}) + return + } + + c.JSON(http.StatusOK, book) +} + +func (h *BookHandlers) GetBooks(c *gin.Context) { + pageStr := c.DefaultQuery("page", "1") + limitStr := c.DefaultQuery("limit", "10") + + page, err := strconv.ParseInt(pageStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid page number"}) + return + } + + limit, err := strconv.ParseInt(limitStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid limit"}) + return + } + + ctx := c.Request.Context() + books, err := h.service.GetBooks(ctx, page, limit) + if err != nil { + h.log.Error("failed to get books", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books"}) + return + } + + c.JSON(http.StatusOK, books) +} + +func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { + bookshelfIDHex := c.Param("bookshelfId") + + bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + return + } + + pageStr := c.DefaultQuery("page", "1") + limitStr := c.DefaultQuery("limit", "10") + + page, err := strconv.ParseInt(pageStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid page number"}) + return + } + + limit, err := strconv.ParseInt(limitStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid limit"}) + return + } + + ctx := c.Request.Context() + books, err := h.service.GetBooksByBookshelfID(ctx, bookshelfID, page, limit) + if err != nil { + h.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books by bookshelf ID"}) + return + } + + c.JSON(http.StatusOK, books) +} + +func (h *BookHandlers) UpdateBook(c *gin.Context) { + bookIDHex := c.Param("id") + + bookID, err := primitive.ObjectIDFromHex(bookIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) + return + } + + var update models.Book + if err := c.BindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx := c.Request.Context() + err = h.service.UpdateBook(ctx, bookID, update.ToMap()) + if err != nil { + if errors.Is(err, services.ErrBookNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to update book", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update book"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Book updated successfully"}) +} + +func (h *BookHandlers) DeleteBook(c *gin.Context) { + bookIDHex := c.Param("id") + + bookID, err := primitive.ObjectIDFromHex(bookIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) + return + } + + ctx := c.Request.Context() + err = h.service.DeleteBook(ctx, bookID) + if err != nil { + if errors.Is(err, services.ErrBookNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to delete book", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete book"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Book deleted successfully"}) +} diff --git a/internal/server/handlers/bookshelves.go b/internal/server/handlers/bookshelves.go new file mode 100644 index 0000000..c59ae6b --- /dev/null +++ b/internal/server/handlers/bookshelves.go @@ -0,0 +1,167 @@ +package handlers + +import ( + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson/primitive" + "log/slog" + "net/http" + "strconv" +) + +type BookshelfHandlers struct { + service *services.BookshelfService + log *slog.Logger +} + +func NewBookshelfHandlers(service *services.BookshelfService, log *slog.Logger) *BookshelfHandlers { + return &BookshelfHandlers{ + service: service, + log: log, + } +} + +func (h *BookshelfHandlers) CreateBookshelf(c *gin.Context) { + userID, exists := c.Get("uid") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var bookshelf models.Bookshelf + if err := c.BindJSON(&bookshelf); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + bookshelf.UserID = userID.(string) // Type assertion to string + ctx := c.Request.Context() + createdBookshelf, err := h.service.CreateBookshelf(ctx, &bookshelf) + if err != nil { + h.log.Error("failed to create bookshelf", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bookshelf"}) + return + } + + c.JSON(http.StatusCreated, createdBookshelf) +} + +func (h *BookshelfHandlers) GetBookshelf(c *gin.Context) { + bookshelfIDHex := c.Param("id") + + bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + return + } + + ctx := c.Request.Context() + bookshelf, err := h.service.GetBookshelf(ctx, bookshelfID) + if err != nil { + if errors.Is(err, services.ErrBookshelfNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to get bookshelf", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelf"}) + return + } + + c.JSON(http.StatusOK, bookshelf) +} + +func (h *BookshelfHandlers) GetBookshelvesByUserID(c *gin.Context) { + userIDHex, exists := c.Get("uid") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + userID, err := primitive.ObjectIDFromHex(fmt.Sprintf("%v", userIDHex)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error parsing user id"}) + return + } + + pageStr := c.DefaultQuery("page", "1") + limitStr := c.DefaultQuery("limit", "10") + + page, err := strconv.ParseInt(pageStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid page number"}) + return + } + + limit, err := strconv.ParseInt(limitStr, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid limit"}) + return + } + + ctx := c.Request.Context() + bookshelves, err := h.service.GetBookshelvesByUserID(ctx, userID, page, limit) + if err != nil { + h.log.Error("failed to get bookshelves by user id", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelves by user ID"}) + return + } + + c.JSON(http.StatusOK, bookshelves) +} + +func (h *BookshelfHandlers) UpdateBookshelf(c *gin.Context) { + bookshelfIDHex := c.Param("id") + + bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + return + } + + var update models.Bookshelf + if err := c.BindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx := c.Request.Context() + err = h.service.UpdateBookshelf(ctx, bookshelfID, update.ToMap()) + if err != nil { + if errors.Is(err, services.ErrBookshelfNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to update bookshelf", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bookshelf"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Bookshelf updated successfully"}) +} + +func (h *BookshelfHandlers) DeleteBookshelf(c *gin.Context) { + bookshelfIDHex := c.Param("id") + + bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + return + } + + ctx := c.Request.Context() + err = h.service.DeleteBookshelf(ctx, bookshelfID) + if err != nil { + if errors.Is(err, services.ErrBookshelfNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to delete bookshelf", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bookshelf"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Bookshelf deleted successfully"}) +} diff --git a/internal/server/handlers/users.go b/internal/server/handlers/users.go new file mode 100644 index 0000000..3fe31aa --- /dev/null +++ b/internal/server/handlers/users.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "errors" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/gin-gonic/gin" + "log/slog" + "net/http" +) + +type UserHandlers struct { + service *services.UserService + log *slog.Logger +} + +func NewUserHandlers(service *services.UserService, log *slog.Logger) *UserHandlers { + return &UserHandlers{ + service: service, + log: log, + } +} + +func (h *UserHandlers) CreateUser(c *gin.Context) { + userID, exists := c.Get("uid") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var user models.User + if err := c.BindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user.ID = userID.(string) // Assign Firebase UID to user.ID + + ctx := c.Request.Context() + createdUser, err := h.service.CreateUser(ctx, &user) + if err != nil { + if errors.Is(err, services.ErrUserAlreadyExists) { + c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to create user", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) + return + } + + c.JSON(http.StatusCreated, createdUser) +} + +func (h *UserHandlers) GetUser(c *gin.Context) { + userID := c.Param("id") // Get userID directly from the URL + + ctx := c.Request.Context() + user, err := h.service.GetUserByID(ctx, userID) + if err != nil { + if errors.Is(err, services.ErrUserNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to get user", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"}) + return + } + + c.JSON(http.StatusOK, user) +} + +func (h *UserHandlers) UpdateUser(c *gin.Context) { + userID := c.Param("id") + + var update models.User + if err := c.BindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx := c.Request.Context() + err := h.service.UpdateUser(ctx, userID, update.ToMap()) + if err != nil { + if errors.Is(err, services.ErrUserNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to update user", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"}) +} + +func (h *UserHandlers) DeleteUser(c *gin.Context) { + userID := c.Param("id") + + ctx := c.Request.Context() + err := h.service.DeleteUser(ctx, userID) + if err != nil { + if errors.Is(err, services.ErrUserNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to delete user", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) +} diff --git a/internal/server/models/book.go b/internal/server/models/book.go index 7eb88f6..d23cc82 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -1,6 +1,7 @@ package models import ( + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "time" ) @@ -8,6 +9,7 @@ import ( // Book represents a book in the library. type Book struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + BookshelfID primitive.ObjectID `bson:"bookshelf_id" json:"bookshelf_id"` Title string `bson:"title" json:"title"` Author string `bson:"author" json:"author"` ISBN string `bson:"isbn" json:"isbn"` @@ -16,3 +18,17 @@ type Book struct { CreatedAt time.Time `bson:"created_at" json:"created_at"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } + +// ToMap converts the Book struct to a bson.M map for MongoDB updates. +func (b *Book) ToMap() bson.M { + return bson.M{ + "bookshelf_id": b.BookshelfID, + "title": b.Title, + "author": b.Author, + "isbn": b.ISBN, + "description": b.Description, + "cover_image": b.CoverImage, + "created_at": b.CreatedAt, + "updated_at": b.UpdatedAt, + } +} diff --git a/internal/server/models/bookshelf.go b/internal/server/models/bookshelf.go index 67870df..c4f4461 100644 --- a/internal/server/models/bookshelf.go +++ b/internal/server/models/bookshelf.go @@ -1,6 +1,7 @@ package models import ( + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "time" ) @@ -8,8 +9,18 @@ import ( // Bookshelf represents a collection of books. type Bookshelf struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` - UserID primitive.ObjectID `bson:"user_id" json:"user_id"` + UserID string `bson:"user_id" json:"user_id"` Name string `bson:"name" json:"name"` CreatedAt time.Time `bson:"created_at" json:"created_at"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } + +// ToMap converts the Bookshelf struct to a bson.M map for MongoDB updates. +func (b *Bookshelf) ToMap() bson.M { + return bson.M{ + "user_id": b.UserID, + "name": b.Name, + "created_at": b.CreatedAt, + "updated_at": b.UpdatedAt, + } +} diff --git a/internal/server/models/user.go b/internal/server/models/user.go index e593316..a50b246 100644 --- a/internal/server/models/user.go +++ b/internal/server/models/user.go @@ -7,11 +7,10 @@ import ( // User represents a user in the system. type User struct { - ID string `bson:"_id" json:"id"` // Firebase UID as primary key - DisplayName string `bson:"display_name" json:"display_name"` - // Add other fields as needed - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + ID string `bson:"_id" json:"id"` // Firebase UID as primary key + DisplayName string `bson:"display_name" json:"display_name"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } func (u *User) ToMap() bson.M { From ac51c60560d080ca3b1db58a6c6b2efe1c305bd5 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 19:27:38 +0300 Subject: [PATCH 15/56] define routes and connect handlers & middlewares --- internal/server/handlers/demo.go | 3 ++ internal/server/handlers/login.go | 4 +- internal/server/middlewares/auth.go | 52 ++++++++++--------- internal/server/routes/routes.go | 47 +++++++++++++++++ internal/server/server.go | 6 +-- .../server/services/{ => auth}/firebase.go | 2 +- .../server/{ => services}/storage/mongo.go | 0 7 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 internal/server/routes/routes.go rename internal/server/services/{ => auth}/firebase.go (96%) rename internal/server/{ => services}/storage/mongo.go (100%) diff --git a/internal/server/handlers/demo.go b/internal/server/handlers/demo.go index 061af56..41732bc 100644 --- a/internal/server/handlers/demo.go +++ b/internal/server/handlers/demo.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "log" "net/http" @@ -9,6 +10,8 @@ import ( func DemoHandler(c *gin.Context) { foo := c.Query("foo") + id, _ := c.Get("uid") + fmt.Printf("id is %s\n", id) if foo != "bar" { log.Printf("foo: %v", foo) c.JSON(http.StatusTeapot, gin.H{"error": "Not Happy :("}) diff --git a/internal/server/handlers/login.go b/internal/server/handlers/login.go index ef06ec0..cd1c8dc 100644 --- a/internal/server/handlers/login.go +++ b/internal/server/handlers/login.go @@ -2,7 +2,7 @@ package handlers import ( "context" - "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "net/http" "github.com/gin-gonic/gin" @@ -18,7 +18,7 @@ func LoginHandler(c *gin.Context) { return } - token, err := services.VerifyIDToken(context.Background(), body.Token) + token, err := auth.VerifyIDToken(context.Background(), body.Token) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) return diff --git a/internal/server/middlewares/auth.go b/internal/server/middlewares/auth.go index 38d934d..bd891b6 100644 --- a/internal/server/middlewares/auth.go +++ b/internal/server/middlewares/auth.go @@ -2,38 +2,40 @@ package middlewares import ( "context" + "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "net/http" "strings" - "github.com/getz-devs/librakeeper-server/internal/server/services" "github.com/gin-gonic/gin" ) -func AuthMiddleware(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) - c.Abort() - return - } +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) + c.Abort() + return + } - // Strip the "Bearer " prefix from the Authorization header value - if !strings.HasPrefix(authHeader, "Bearer ") { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) - c.Abort() - return - } - idToken := strings.TrimPrefix(authHeader, "Bearer ") + // Strip the "Bearer " prefix from the Authorization header value + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"}) + c.Abort() + return + } + idToken := strings.TrimPrefix(authHeader, "Bearer ") - // Verify the ID token - token, err := services.VerifyIDToken(context.Background(), idToken) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } + // Verify the ID token + token, err := auth.VerifyIDToken(context.Background(), idToken) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } - // Set the UID in the context for further use - c.Set("uid", token.UID) - c.Next() + // Set the UID in the context for further use + c.Set("uid", token.UID) + c.Next() + } } diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go new file mode 100644 index 0000000..2487a7d --- /dev/null +++ b/internal/server/routes/routes.go @@ -0,0 +1,47 @@ +package routes + +import ( + "github.com/getz-devs/librakeeper-server/internal/server/handlers" + "github.com/getz-devs/librakeeper-server/internal/server/middlewares" + "github.com/gin-gonic/gin" +) + +// Handlers is a struct that groups all handler functions. +type Handlers struct { + Users *handlers.UserHandlers + Bookshelves *handlers.BookshelfHandlers + Books *handlers.BookHandlers +} + +func SetupRoutes(router *gin.Engine, h *Handlers) { + api := router.Group("/api") + + //api.GET("/health", handlers.HealthCheck) + + userGroup := api.Group("/users") + { + userGroup.POST("/", middlewares.AuthMiddleware(), h.Users.CreateUser) + userGroup.GET("/:id", middlewares.AuthMiddleware(), h.Users.GetUser) + userGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Users.UpdateUser) + userGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Users.DeleteUser) + } + + bookshelvesGroup := api.Group("/bookshelves") + { + bookshelvesGroup.POST("/", middlewares.AuthMiddleware(), h.Bookshelves.CreateBookshelf) + bookshelvesGroup.GET("/:id", h.Bookshelves.GetBookshelf) + bookshelvesGroup.GET("/user", middlewares.AuthMiddleware(), h.Bookshelves.GetBookshelvesByUserID) + bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.UpdateBookshelf) + bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.DeleteBookshelf) + } + + booksGroup := api.Group("/books") + { + booksGroup.POST("/", h.Books.CreateBook) + booksGroup.GET("/:id", h.Books.GetBook) + booksGroup.GET("/", h.Books.GetBooks) + booksGroup.GET("/bookshelf/:bookshelfId", h.Books.GetBooksByBookshelfID) + booksGroup.PUT("/:id", h.Books.UpdateBook) + booksGroup.DELETE("/:id", h.Books.DeleteBook) + } +} diff --git a/internal/server/server.go b/internal/server/server.go index c88befe..f7979d6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,7 +3,7 @@ package main import ( "github.com/getz-devs/librakeeper-server/internal/server/handlers" "github.com/getz-devs/librakeeper-server/internal/server/middlewares" - "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log" @@ -23,14 +23,14 @@ func main() { r.Use(cors.New(config)) // Initialize Firebase - err := services.InitializeFirebase("internal/server/.secret.json") // TODO: read config + err := auth.InitializeFirebase("internal/server/.secret.json") // TODO: read config if err != nil { log.Fatalf("error initializing Firebase: %v", err) } // Routes r.POST("/login", handlers.LoginHandler) - r.GET("/demo", middlewares.AuthMiddleware, handlers.DemoHandler) + r.GET("/demo", middlewares.AuthMiddleware(), handlers.DemoHandler) err = r.Run(":8080") // TODO: read config if err != nil { diff --git a/internal/server/services/firebase.go b/internal/server/services/auth/firebase.go similarity index 96% rename from internal/server/services/firebase.go rename to internal/server/services/auth/firebase.go index b7db416..d6dd4c6 100644 --- a/internal/server/services/firebase.go +++ b/internal/server/services/auth/firebase.go @@ -1,4 +1,4 @@ -package services +package auth import ( "context" diff --git a/internal/server/storage/mongo.go b/internal/server/services/storage/mongo.go similarity index 100% rename from internal/server/storage/mongo.go rename to internal/server/services/storage/mongo.go From 390107250ed25c5d4785011c6121be2991e690c0 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 20:08:41 +0300 Subject: [PATCH 16/56] improve storage service and finalize main cmd --- cmd/server/main.go | 73 ++++++++++++++++++----- internal/server/server.go | 8 +-- internal/server/services/storage/mongo.go | 37 ++++++++++-- 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index f91e030..3671579 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,42 +1,87 @@ package main import ( + "context" + "errors" + "fmt" "github.com/getz-devs/librakeeper-server/internal/server/config" + "github.com/getz-devs/librakeeper-server/internal/server/handlers" + "github.com/getz-devs/librakeeper-server/internal/server/routes" + "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/storage" "github.com/getz-devs/librakeeper-server/lib/prettylog" + "github.com/gin-gonic/gin" "log/slog" + "net/http" "os" -) - -const ( - envLocal = "local" - envDev = "dev" - envProd = "prod" + "os/signal" + "syscall" + "time" ) func main() { cfg := config.MustLoad() log := setupLogger(cfg.Env) + log.Info("starting librakeeper server", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) + + _, collections := storage.Initialize(cfg, log) + + userService := services.NewUserService(collections.UsersCollection, log) + bookshelfService := services.NewBookshelfService(collections.BookshelvesCollection, log) + bookService := services.NewBookService(collections.BooksCollection, log) + + h := &routes.Handlers{ + Users: handlers.NewUserHandlers(userService, log), + Bookshelves: handlers.NewBookshelfHandlers(bookshelfService, log), + Books: handlers.NewBookHandlers(bookService, log), + } + + router := gin.New() + router.Use(gin.Logger(), gin.Recovery()) + + routes.SetupRoutes(router, h) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", cfg.Server.Port), + Handler: router, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Error("failed to start server", slog.Any("error", err)) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Info("shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Error("server forced to shutdown", slog.Any("error", err)) + } - log.Info("starting ...", - slog.String("env", cfg.Env), - slog.Int("port", cfg.ServerConfig.Port), - ) + log.Info("server exiting") } func setupLogger(env string) *slog.Logger { var log *slog.Logger switch env { - case envLocal: + case "local": log = slog.New(prettylog.NewHandler(&slog.HandlerOptions{ - Level: slog.LevelInfo, + Level: slog.LevelDebug, AddSource: false, ReplaceAttr: nil, })) - case envDev: + case "development": log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - case envProd: + case "production": log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) } diff --git a/internal/server/server.go b/internal/server/server.go index f7979d6..7d533b3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -14,10 +14,10 @@ func main() { // Configure CORS config := cors.Config{ - AllowOrigins: []string{"http://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization - AllowCredentials: true, // Allow credentials + AllowOrigins: []string{"https://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization + AllowCredentials: true, // Allow credentials } r.Use(cors.New(config)) diff --git a/internal/server/services/storage/mongo.go b/internal/server/services/storage/mongo.go index e21a0e0..01a529c 100644 --- a/internal/server/services/storage/mongo.go +++ b/internal/server/services/storage/mongo.go @@ -1,4 +1,4 @@ -package database +package storage import ( "context" @@ -11,12 +11,19 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +type Collections struct { + UsersCollection *mongo.Collection + BooksCollection *mongo.Collection + BookshelvesCollection *mongo.Collection +} + var ( - _db *mongo.Database - _log *slog.Logger + _log *slog.Logger + _db *mongo.Database + _collections Collections ) -func Initialize(cfg *config.Config, log *slog.Logger) *mongo.Database { +func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, Collections) { _log = log ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -29,7 +36,12 @@ func Initialize(cfg *config.Config, log *slog.Logger) *mongo.Database { _db = client.Database(cfg.Database) _log.Info("connected to mongodb", slog.String("database", cfg.Database)) - return _db + + _collections.UsersCollection = _db.Collection("users") + _collections.BooksCollection = _db.Collection("books") + _collections.BookshelvesCollection = _db.Collection("bookshelves") + + return _db, _collections } func GetCollection(name string) *mongo.Collection { @@ -39,3 +51,18 @@ func GetCollection(name string) *mongo.Collection { return _db.Collection(name) } + +// Ping checks the database connectivity. +func Ping(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // Use the context's timeout + defer cancel() + + if _db == nil { + panic(fmt.Errorf("mongodb has not been initialized")) + } + + if err := _db.Client().Ping(ctx, nil); err != nil { + return fmt.Errorf("mongodb ping failed: %w", err) + } + return nil +} From e256539aaa83184cee609add74364fe93d23084c Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 21:10:08 +0300 Subject: [PATCH 17/56] fix: load firebase config --- cmd/server/main.go | 7 +++++++ go.sum | 16 ++++++++++++++++ internal/server/config/config.go | 13 +++++++++---- internal/server/middlewares/auth.go | 2 +- internal/server/services/users.go | 1 - 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3671579..a6a43e7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,6 +8,7 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/handlers" "github.com/getz-devs/librakeeper-server/internal/server/routes" "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "github.com/getz-devs/librakeeper-server/internal/server/services/storage" "github.com/getz-devs/librakeeper-server/lib/prettylog" "github.com/gin-gonic/gin" @@ -25,6 +26,12 @@ func main() { log := setupLogger(cfg.Env) log.Info("starting librakeeper server", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) + err := auth.InitializeFirebase(cfg.FirebaseConfigPath) + if err != nil { + log.Error("failed to initialize Firebase", slog.Any("error", err)) + // TODO: Handle the error appropriately (e.g., panic or graceful shutdown) + } + _, collections := storage.Initialize(cfg, log) userService := services.NewUserService(collections.UsersCollection, log) diff --git a/go.sum b/go.sum index 0e95352..fca07a0 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -104,6 +106,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -117,6 +121,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -136,7 +142,17 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4= +go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck= diff --git a/internal/server/config/config.go b/internal/server/config/config.go index e74ead5..a674745 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -10,10 +10,11 @@ import ( // Config represents the application's configuration. type Config struct { - Env string `yaml:"env" env-default:"local"` - MongoURI string `yaml:"mongo_uri" env-required:"true"` - Database string `yaml:"database" env-required:"true"` - Server struct { + Env string `yaml:"env" env-default:"local"` + MongoURI string `yaml:"mongo_uri" env-required:"true"` + FirebaseConfigPath string `yaml:"firebase_config" env-required:"true"` + Database string `yaml:"database" env-required:"true"` + Server struct { Port int `yaml:"port" env-default:"8080"` } `yaml:"server"` } @@ -48,5 +49,9 @@ func fetchConfigPath() string { configPath = os.Getenv("CONFIG_PATH") } + if configPath == "" { + panic("no config file path provided") + } + return configPath } diff --git a/internal/server/middlewares/auth.go b/internal/server/middlewares/auth.go index bd891b6..275b36c 100644 --- a/internal/server/middlewares/auth.go +++ b/internal/server/middlewares/auth.go @@ -29,7 +29,7 @@ func AuthMiddleware() gin.HandlerFunc { // Verify the ID token token, err := auth.VerifyIDToken(context.Background(), idToken) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized", "message": err.Error()}) c.Abort() return } diff --git a/internal/server/services/users.go b/internal/server/services/users.go index 29bb339..31f75be 100644 --- a/internal/server/services/users.go +++ b/internal/server/services/users.go @@ -40,7 +40,6 @@ func (s *UserService) CreateUser(ctx context.Context, user *models.User) (*model return nil, ErrUserAlreadyExists } - s.log.Error("failed to create user", slog.Any("error", err)) return nil, fmt.Errorf("failed to create user: %w", err) } From 14f12748edb1cb5e47e35ea7de10e2bcc35b63eb Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 2 Jul 2024 21:18:11 +0300 Subject: [PATCH 18/56] add health endpoint, remove old code --- cmd/server/main.go | 11 +++++++- internal/server/handlers/demo.go | 43 ------------------------------ internal/server/handlers/health.go | 28 +++++++++++++++++++ internal/server/handlers/login.go | 28 ------------------- internal/server/routes/routes.go | 2 +- internal/server/server.go | 7 ++--- 6 files changed, 43 insertions(+), 76 deletions(-) delete mode 100644 internal/server/handlers/demo.go create mode 100644 internal/server/handlers/health.go delete mode 100644 internal/server/handlers/login.go diff --git a/cmd/server/main.go b/cmd/server/main.go index a6a43e7..0052d0a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,6 +11,7 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "github.com/getz-devs/librakeeper-server/internal/server/services/storage" "github.com/getz-devs/librakeeper-server/lib/prettylog" + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log/slog" "net/http" @@ -44,8 +45,16 @@ func main() { Books: handlers.NewBookHandlers(bookService, log), } + // Configure CORS + corsConfig := cors.Config{ + AllowOrigins: []string{"https://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization + AllowCredentials: true, // Allow credentials + } + router := gin.New() - router.Use(gin.Logger(), gin.Recovery()) + router.Use(gin.Logger(), gin.Recovery(), cors.New(corsConfig)) routes.SetupRoutes(router, h) diff --git a/internal/server/handlers/demo.go b/internal/server/handlers/demo.go deleted file mode 100644 index 41732bc..0000000 --- a/internal/server/handlers/demo.go +++ /dev/null @@ -1,43 +0,0 @@ -package handlers - -import ( - "fmt" - "log" - "net/http" - - "github.com/gin-gonic/gin" -) - -func DemoHandler(c *gin.Context) { - foo := c.Query("foo") - id, _ := c.Get("uid") - fmt.Printf("id is %s\n", id) - if foo != "bar" { - log.Printf("foo: %v", foo) - c.JSON(http.StatusTeapot, gin.H{"error": "Not Happy :("}) - return - } - - c.JSON(http.StatusOK, gin.H{"info": "Happy :)"}) -} - -/* -func CreateTodo(c *gin.Context) { - uid, exists := c.Get("uid") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - var body struct { - Todo string `json:"todo"` - } - if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return - } - - // Save the todo for the user with UID 'uid' - c.JSON(http.StatusOK, gin.H{"todo": body.Todo}) -} -*/ diff --git a/internal/server/handlers/health.go b/internal/server/handlers/health.go new file mode 100644 index 0000000..74227bf --- /dev/null +++ b/internal/server/handlers/health.go @@ -0,0 +1,28 @@ +package handlers + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/services/storage" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// HealthCheck performs a more comprehensive health check. +func HealthCheck(c *gin.Context) { + // Check Database Connectivity + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Short timeout for DB check + defer cancel() + + err := storage.Ping(ctx) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"status": "DOWN", "error": "Database connection failed"}) + return + } + + // TODO: Add other checks here as needed (e.g., external services, dependencies) + + // If all checks pass + c.JSON(http.StatusOK, gin.H{"status": "OK"}) +} diff --git a/internal/server/handlers/login.go b/internal/server/handlers/login.go deleted file mode 100644 index cd1c8dc..0000000 --- a/internal/server/handlers/login.go +++ /dev/null @@ -1,28 +0,0 @@ -package handlers - -import ( - "context" - "github.com/getz-devs/librakeeper-server/internal/server/services/auth" - "net/http" - - "github.com/gin-gonic/gin" -) - -func LoginHandler(c *gin.Context) { - var body struct { - Token string `json:"token"` - } - - if err := c.BindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return - } - - token, err := auth.VerifyIDToken(context.Background(), body.Token) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - return - } - - c.JSON(http.StatusOK, gin.H{"uid": token.UID}) -} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 2487a7d..b0fdd07 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -16,7 +16,7 @@ type Handlers struct { func SetupRoutes(router *gin.Engine, h *Handlers) { api := router.Group("/api") - //api.GET("/health", handlers.HealthCheck) + api.GET("/health", handlers.HealthCheck) userGroup := api.Group("/users") { diff --git a/internal/server/server.go b/internal/server/server.go index 7d533b3..f090992 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,13 +2,15 @@ package main import ( "github.com/getz-devs/librakeeper-server/internal/server/handlers" - "github.com/getz-devs/librakeeper-server/internal/server/middlewares" "github.com/getz-devs/librakeeper-server/internal/server/services/auth" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log" ) +// WARN: old code +// TODO: move some code from cmd here + func main() { r := gin.Default() @@ -29,8 +31,7 @@ func main() { } // Routes - r.POST("/login", handlers.LoginHandler) - r.GET("/demo", middlewares.AuthMiddleware(), handlers.DemoHandler) + r.POST("/health", handlers.HealthCheck) err = r.Run(":8080") // TODO: read config if err != nil { From 09624480050250f46fceb83c8c08986980a1a90e Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 3 Jul 2024 05:56:33 +0300 Subject: [PATCH 19/56] improve main server cmd - move init to server.go --- cmd/server/main.go | 79 ++++-------------------- internal/server/config/config.go | 3 +- internal/server/server.go | 101 ++++++++++++++++++++++++------- 3 files changed, 91 insertions(+), 92 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 0052d0a..224fb76 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,88 +1,31 @@ package main import ( - "context" - "errors" - "fmt" + "github.com/getz-devs/librakeeper-server/internal/server" "github.com/getz-devs/librakeeper-server/internal/server/config" - "github.com/getz-devs/librakeeper-server/internal/server/handlers" - "github.com/getz-devs/librakeeper-server/internal/server/routes" - "github.com/getz-devs/librakeeper-server/internal/server/services" - "github.com/getz-devs/librakeeper-server/internal/server/services/auth" - "github.com/getz-devs/librakeeper-server/internal/server/services/storage" "github.com/getz-devs/librakeeper-server/lib/prettylog" - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" "log/slog" - "net/http" "os" - "os/signal" - "syscall" - "time" ) func main() { cfg := config.MustLoad() log := setupLogger(cfg.Env) - log.Info("starting librakeeper server", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) + log.Info("starting librakeeper srv", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) - err := auth.InitializeFirebase(cfg.FirebaseConfigPath) - if err != nil { - log.Error("failed to initialize Firebase", slog.Any("error", err)) - // TODO: Handle the error appropriately (e.g., panic or graceful shutdown) + // Create and initialize the srv + srv := server.NewServer(cfg, log) + if err := srv.Initialize(); err != nil { + log.Error("failed to initialize srv", slog.Any("error", err)) + os.Exit(1) } - _, collections := storage.Initialize(cfg, log) - - userService := services.NewUserService(collections.UsersCollection, log) - bookshelfService := services.NewBookshelfService(collections.BookshelvesCollection, log) - bookService := services.NewBookService(collections.BooksCollection, log) - - h := &routes.Handlers{ - Users: handlers.NewUserHandlers(userService, log), - Bookshelves: handlers.NewBookshelfHandlers(bookshelfService, log), - Books: handlers.NewBookHandlers(bookService, log), - } - - // Configure CORS - corsConfig := cors.Config{ - AllowOrigins: []string{"https://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization - AllowCredentials: true, // Allow credentials - } - - router := gin.New() - router.Use(gin.Logger(), gin.Recovery(), cors.New(corsConfig)) - - routes.SetupRoutes(router, h) - - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.Server.Port), - Handler: router, + // Run the srv + if err := srv.Run(); err != nil { + log.Error("failed to run srv", slog.Any("error", err)) + os.Exit(1) } - - go func() { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Error("failed to start server", slog.Any("error", err)) - } - }() - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Info("shutting down server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Error("server forced to shutdown", slog.Any("error", err)) - } - - log.Info("server exiting") } func setupLogger(env string) *slog.Logger { diff --git a/internal/server/config/config.go b/internal/server/config/config.go index a674745..fd31afb 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -15,7 +15,8 @@ type Config struct { FirebaseConfigPath string `yaml:"firebase_config" env-required:"true"` Database string `yaml:"database" env-required:"true"` Server struct { - Port int `yaml:"port" env-default:"8080"` + Port int `yaml:"port" env-default:"8080"` + AllowedOrigins []string `yaml:"allowed_origins"` } `yaml:"server"` } diff --git a/internal/server/server.go b/internal/server/server.go index f090992..0d5a741 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,41 +1,96 @@ -package main +package server import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/config" "github.com/getz-devs/librakeeper-server/internal/server/handlers" + "github.com/getz-devs/librakeeper-server/internal/server/routes" + "github.com/getz-devs/librakeeper-server/internal/server/services" "github.com/getz-devs/librakeeper-server/internal/server/services/auth" + "github.com/getz-devs/librakeeper-server/internal/server/services/storage" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "log" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" ) -// WARN: old code -// TODO: move some code from cmd here - -func main() { - r := gin.Default() +// Server represents the API server. +type Server struct { + config *config.Config + log *slog.Logger + router *gin.Engine + httpServer *http.Server +} - // Configure CORS - config := cors.Config{ - AllowOrigins: []string{"https://libra.potat.dev", "http://localhost:3000"}, // Allow specific origins - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allow methods - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, // Allow headers including Authorization - AllowCredentials: true, // Allow credentials +// NewServer creates a new Server instance. +func NewServer(config *config.Config, log *slog.Logger) *Server { + return &Server{ + config: config, + log: log, + router: gin.New(), } +} - r.Use(cors.New(config)) - +// Initialize initializes the server components. +func (s *Server) Initialize() error { // Initialize Firebase - err := auth.InitializeFirebase("internal/server/.secret.json") // TODO: read config + err := auth.InitializeFirebase(s.config.FirebaseConfigPath) if err != nil { - log.Fatalf("error initializing Firebase: %v", err) + return fmt.Errorf("failed to initialize Firebase: %w", err) } - // Routes - r.POST("/health", handlers.HealthCheck) + _, collections := storage.Initialize(s.config, s.log) - err = r.Run(":8080") // TODO: read config - if err != nil { - log.Fatalf("error starting server: %v", err) - return + userService := services.NewUserService(collections.UsersCollection, s.log) + bookshelfService := services.NewBookshelfService(collections.BookshelvesCollection, s.log) + bookService := services.NewBookService(collections.BooksCollection, s.log) + + h := &routes.Handlers{ + Users: handlers.NewUserHandlers(userService, s.log), + Bookshelves: handlers.NewBookshelfHandlers(bookshelfService, s.log), + Books: handlers.NewBookHandlers(bookService, s.log), + } + + // Configure CORS + corsConfig := cors.Config{ + AllowOrigins: s.config.Server.AllowedOrigins, // Get origins from config + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + AllowCredentials: true, } + s.router.Use(gin.Logger(), gin.Recovery(), cors.New(corsConfig)) + routes.SetupRoutes(s.router, h) + + s.httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", s.config.Server.Port), + Handler: s.router, + } + + return nil +} + +// Run starts the HTTP server and handles graceful shutdown. +func (s *Server) Run() error { + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.log.Error("failed to start server", slog.Any("error", err)) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + s.log.Info("shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return s.httpServer.Shutdown(ctx) } From 13f54c202fa3ab5eb8edbf54cafbe19ddc9c345b Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 3 Jul 2024 06:17:30 +0300 Subject: [PATCH 20/56] improve config file structure --- cmd/server/config.example.yaml | 11 +++++++++++ internal/server/config/config.go | 17 ++++++++++++----- internal/server/server.go | 2 +- internal/server/services/storage/mongo.go | 14 +++----------- 4 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 cmd/server/config.example.yaml diff --git a/cmd/server/config.example.yaml b/cmd/server/config.example.yaml new file mode 100644 index 0000000..abeb554 --- /dev/null +++ b/cmd/server/config.example.yaml @@ -0,0 +1,11 @@ +server: + allowed_origins: + - https://libra.example.com + - http://localhost:3000 + +database: + uri: mongodb://user:pass@cool.cloud.com:1234 + name: database_name + +auth: + config_path: firebase.json diff --git a/internal/server/config/config.go b/internal/server/config/config.go index fd31afb..3ac5047 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -10,14 +10,21 @@ import ( // Config represents the application's configuration. type Config struct { - Env string `yaml:"env" env-default:"local"` - MongoURI string `yaml:"mongo_uri" env-required:"true"` - FirebaseConfigPath string `yaml:"firebase_config" env-required:"true"` - Database string `yaml:"database" env-required:"true"` - Server struct { + Env string `yaml:"env" env-default:"local"` + + Server struct { Port int `yaml:"port" env-default:"8080"` AllowedOrigins []string `yaml:"allowed_origins"` } `yaml:"server"` + + Database struct { + URI string `yaml:"uri" env-required:"true"` + Name string `yaml:"name" env-required:"true"` + } `yaml:"database"` + + Auth struct { + ConfigPath string `yaml:"config_path" env-required:"true"` + } `yaml:"auth"` } // MustLoad loads the configuration from the specified path and environment variables. diff --git a/internal/server/server.go b/internal/server/server.go index 0d5a741..d70ef10 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -40,7 +40,7 @@ func NewServer(config *config.Config, log *slog.Logger) *Server { // Initialize initializes the server components. func (s *Server) Initialize() error { // Initialize Firebase - err := auth.InitializeFirebase(s.config.FirebaseConfigPath) + err := auth.InitializeFirebase(s.config.Auth.ConfigPath) if err != nil { return fmt.Errorf("failed to initialize Firebase: %w", err) } diff --git a/internal/server/services/storage/mongo.go b/internal/server/services/storage/mongo.go index 01a529c..77e768b 100644 --- a/internal/server/services/storage/mongo.go +++ b/internal/server/services/storage/mongo.go @@ -28,14 +28,14 @@ func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, Collecti ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.MongoURI)) + client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.Database.URI)) if err != nil { _log.Error("failed to connect to mongodb", slog.Any("error", err)) panic(err) } - _db = client.Database(cfg.Database) - _log.Info("connected to mongodb", slog.String("database", cfg.Database)) + _db = client.Database(cfg.Database.Name) + _log.Info("connected to mongodb", slog.String("database", cfg.Database.Name)) _collections.UsersCollection = _db.Collection("users") _collections.BooksCollection = _db.Collection("books") @@ -44,14 +44,6 @@ func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, Collecti return _db, _collections } -func GetCollection(name string) *mongo.Collection { - if _db == nil { - panic(fmt.Errorf("mongodb has not been initialized")) - } - - return _db.Collection(name) -} - // Ping checks the database connectivity. func Ping(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // Use the context's timeout From a2cce300288bfaa2d165543436cc33f4ca42197f Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 3 Jul 2024 06:28:20 +0300 Subject: [PATCH 21/56] move services to separate packages --- internal/server/handlers/books.go | 28 +++++++++---------- internal/server/handlers/bookshelves.go | 20 ++++++------- internal/server/handlers/users.go | 14 +++++----- internal/server/server.go | 10 ++++--- internal/server/services/{ => books}/books.go | 2 +- .../services/{ => bookshelves}/bookshelves.go | 2 +- internal/server/services/{ => users}/users.go | 2 +- 7 files changed, 40 insertions(+), 38 deletions(-) rename internal/server/services/{ => books}/books.go (99%) rename internal/server/services/{ => bookshelves}/bookshelves.go (99%) rename internal/server/services/{ => users}/users.go (99%) diff --git a/internal/server/handlers/books.go b/internal/server/handlers/books.go index 6d67032..106b1a5 100644 --- a/internal/server/handlers/books.go +++ b/internal/server/handlers/books.go @@ -3,7 +3,7 @@ package handlers import ( "errors" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/books" "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson/primitive" "log/slog" @@ -12,11 +12,11 @@ import ( ) type BookHandlers struct { - service *services.BookService + service *books.BookService log *slog.Logger } -func NewBookHandlers(service *services.BookService, log *slog.Logger) *BookHandlers { +func NewBookHandlers(service *books.BookService, log *slog.Logger) *BookHandlers { return &BookHandlers{ service: service, log: log, @@ -53,7 +53,7 @@ func (h *BookHandlers) GetBook(c *gin.Context) { ctx := c.Request.Context() book, err := h.service.GetBook(ctx, bookID) if err != nil { - if errors.Is(err, services.ErrBookNotFound) { + if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -82,14 +82,14 @@ func (h *BookHandlers) GetBooks(c *gin.Context) { } ctx := c.Request.Context() - books, err := h.service.GetBooks(ctx, page, limit) + result, err := h.service.GetBooks(ctx, page, limit) if err != nil { - h.log.Error("failed to get books", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books"}) + h.log.Error("failed to get result", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result"}) return } - c.JSON(http.StatusOK, books) + c.JSON(http.StatusOK, result) } func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { @@ -117,14 +117,14 @@ func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { } ctx := c.Request.Context() - books, err := h.service.GetBooksByBookshelfID(ctx, bookshelfID, page, limit) + result, err := h.service.GetBooksByBookshelfID(ctx, bookshelfID, page, limit) if err != nil { - h.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books by bookshelf ID"}) + h.log.Error("failed to get result by bookshelf id", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result by bookshelf ID"}) return } - c.JSON(http.StatusOK, books) + c.JSON(http.StatusOK, result) } func (h *BookHandlers) UpdateBook(c *gin.Context) { @@ -145,7 +145,7 @@ func (h *BookHandlers) UpdateBook(c *gin.Context) { ctx := c.Request.Context() err = h.service.UpdateBook(ctx, bookID, update.ToMap()) if err != nil { - if errors.Is(err, services.ErrBookNotFound) { + if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -169,7 +169,7 @@ func (h *BookHandlers) DeleteBook(c *gin.Context) { ctx := c.Request.Context() err = h.service.DeleteBook(ctx, bookID) if err != nil { - if errors.Is(err, services.ErrBookNotFound) { + if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/handlers/bookshelves.go b/internal/server/handlers/bookshelves.go index c59ae6b..2c5718e 100644 --- a/internal/server/handlers/bookshelves.go +++ b/internal/server/handlers/bookshelves.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelves" "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson/primitive" "log/slog" @@ -13,11 +13,11 @@ import ( ) type BookshelfHandlers struct { - service *services.BookshelfService + service *bookshelves.BookshelfService log *slog.Logger } -func NewBookshelfHandlers(service *services.BookshelfService, log *slog.Logger) *BookshelfHandlers { +func NewBookshelfHandlers(service *bookshelves.BookshelfService, log *slog.Logger) *BookshelfHandlers { return &BookshelfHandlers{ service: service, log: log, @@ -61,7 +61,7 @@ func (h *BookshelfHandlers) GetBookshelf(c *gin.Context) { ctx := c.Request.Context() bookshelf, err := h.service.GetBookshelf(ctx, bookshelfID) if err != nil { - if errors.Is(err, services.ErrBookshelfNotFound) { + if errors.Is(err, bookshelves.ErrBookshelfNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -102,14 +102,14 @@ func (h *BookshelfHandlers) GetBookshelvesByUserID(c *gin.Context) { } ctx := c.Request.Context() - bookshelves, err := h.service.GetBookshelvesByUserID(ctx, userID, page, limit) + result, err := h.service.GetBookshelvesByUserID(ctx, userID, page, limit) if err != nil { - h.log.Error("failed to get bookshelves by user id", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelves by user ID"}) + h.log.Error("failed to get result by user id", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result by user ID"}) return } - c.JSON(http.StatusOK, bookshelves) + c.JSON(http.StatusOK, result) } func (h *BookshelfHandlers) UpdateBookshelf(c *gin.Context) { @@ -130,7 +130,7 @@ func (h *BookshelfHandlers) UpdateBookshelf(c *gin.Context) { ctx := c.Request.Context() err = h.service.UpdateBookshelf(ctx, bookshelfID, update.ToMap()) if err != nil { - if errors.Is(err, services.ErrBookshelfNotFound) { + if errors.Is(err, bookshelves.ErrBookshelfNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -154,7 +154,7 @@ func (h *BookshelfHandlers) DeleteBookshelf(c *gin.Context) { ctx := c.Request.Context() err = h.service.DeleteBookshelf(ctx, bookshelfID) if err != nil { - if errors.Is(err, services.ErrBookshelfNotFound) { + if errors.Is(err, bookshelves.ErrBookshelfNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/handlers/users.go b/internal/server/handlers/users.go index 3fe31aa..df798e6 100644 --- a/internal/server/handlers/users.go +++ b/internal/server/handlers/users.go @@ -3,18 +3,18 @@ package handlers import ( "errors" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services" + "github.com/getz-devs/librakeeper-server/internal/server/services/users" "github.com/gin-gonic/gin" "log/slog" "net/http" ) type UserHandlers struct { - service *services.UserService + service *users.UserService log *slog.Logger } -func NewUserHandlers(service *services.UserService, log *slog.Logger) *UserHandlers { +func NewUserHandlers(service *users.UserService, log *slog.Logger) *UserHandlers { return &UserHandlers{ service: service, log: log, @@ -39,7 +39,7 @@ func (h *UserHandlers) CreateUser(c *gin.Context) { ctx := c.Request.Context() createdUser, err := h.service.CreateUser(ctx, &user) if err != nil { - if errors.Is(err, services.ErrUserAlreadyExists) { + if errors.Is(err, users.ErrUserAlreadyExists) { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } @@ -57,7 +57,7 @@ func (h *UserHandlers) GetUser(c *gin.Context) { ctx := c.Request.Context() user, err := h.service.GetUserByID(ctx, userID) if err != nil { - if errors.Is(err, services.ErrUserNotFound) { + if errors.Is(err, users.ErrUserNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -81,7 +81,7 @@ func (h *UserHandlers) UpdateUser(c *gin.Context) { ctx := c.Request.Context() err := h.service.UpdateUser(ctx, userID, update.ToMap()) if err != nil { - if errors.Is(err, services.ErrUserNotFound) { + if errors.Is(err, users.ErrUserNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -99,7 +99,7 @@ func (h *UserHandlers) DeleteUser(c *gin.Context) { ctx := c.Request.Context() err := h.service.DeleteUser(ctx, userID) if err != nil { - if errors.Is(err, services.ErrUserNotFound) { + if errors.Is(err, users.ErrUserNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/server.go b/internal/server/server.go index d70ef10..8d611f0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,9 +7,11 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/config" "github.com/getz-devs/librakeeper-server/internal/server/handlers" "github.com/getz-devs/librakeeper-server/internal/server/routes" - "github.com/getz-devs/librakeeper-server/internal/server/services" "github.com/getz-devs/librakeeper-server/internal/server/services/auth" + "github.com/getz-devs/librakeeper-server/internal/server/services/books" + "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelves" "github.com/getz-devs/librakeeper-server/internal/server/services/storage" + "github.com/getz-devs/librakeeper-server/internal/server/services/users" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log/slog" @@ -47,9 +49,9 @@ func (s *Server) Initialize() error { _, collections := storage.Initialize(s.config, s.log) - userService := services.NewUserService(collections.UsersCollection, s.log) - bookshelfService := services.NewBookshelfService(collections.BookshelvesCollection, s.log) - bookService := services.NewBookService(collections.BooksCollection, s.log) + userService := users.NewUserService(collections.UsersCollection, s.log) + bookshelfService := bookshelves.NewBookshelfService(collections.BookshelvesCollection, s.log) + bookService := books.NewBookService(collections.BooksCollection, s.log) h := &routes.Handlers{ Users: handlers.NewUserHandlers(userService, s.log), diff --git a/internal/server/services/books.go b/internal/server/services/books/books.go similarity index 99% rename from internal/server/services/books.go rename to internal/server/services/books/books.go index f90981e..4e707fe 100644 --- a/internal/server/services/books.go +++ b/internal/server/services/books/books.go @@ -1,4 +1,4 @@ -package services +package books import ( "context" diff --git a/internal/server/services/bookshelves.go b/internal/server/services/bookshelves/bookshelves.go similarity index 99% rename from internal/server/services/bookshelves.go rename to internal/server/services/bookshelves/bookshelves.go index 62ee5a8..fb82fbf 100644 --- a/internal/server/services/bookshelves.go +++ b/internal/server/services/bookshelves/bookshelves.go @@ -1,4 +1,4 @@ -package services +package bookshelves import ( "context" diff --git a/internal/server/services/users.go b/internal/server/services/users/users.go similarity index 99% rename from internal/server/services/users.go rename to internal/server/services/users/users.go index 31f75be..383124e 100644 --- a/internal/server/services/users.go +++ b/internal/server/services/users/users.go @@ -1,4 +1,4 @@ -package services +package users import ( "context" From 1a4672c13120f728c72457fc8459acbd4f5d4e53 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Wed, 3 Jul 2024 07:15:31 +0300 Subject: [PATCH 22/56] All finished --- cmd/searcher-agent/main.go | 10 ++- cmd/test_main/test_main.go | 12 +++ config/searcher-agent/local.yaml.example | 7 +- go.mod | 2 +- go.sum | 2 + internal/searcher-agent/app/app.go | 16 +++- .../searcher-agent/app/rabbit/app_rabbit.go | 14 +++- internal/searcher-agent/config/config.go | 13 +-- internal/searcher-agent/rabbit/rabbit.go | 60 +++++++++----- .../searcher-agent/storage/mongo/mongo.go | 82 +++++++++++++++++++ internal/searcher/grpc/server.go | 29 ++++++- internal/searcher/storage/mongo/mongo.go | 42 ---------- 12 files changed, 210 insertions(+), 79 deletions(-) create mode 100644 cmd/test_main/test_main.go create mode 100644 internal/searcher-agent/storage/mongo/mongo.go diff --git a/cmd/searcher-agent/main.go b/cmd/searcher-agent/main.go index 91ce0cf..09d6a27 100644 --- a/cmd/searcher-agent/main.go +++ b/cmd/searcher-agent/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/getz-devs/librakeeper-server/internal/searcher-agent/app" "github.com/getz-devs/librakeeper-server/internal/searcher-agent/config" + mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher-agent/storage/mongo" "github.com/getz-devs/librakeeper-server/lib/prettylog" "log/slog" "os" @@ -27,7 +28,13 @@ func main() { slog.Any("config", cfg), ) - application := app.New(cfg.ConnectUrl, cfg.QueueName, log) + databaseMongoConfig := mongostorage.DatabaseMongoConfig{ + ConnectUrl: cfg.DatabaseMongo.ConnectURL, + Database: cfg.DatabaseMongo.DatabaseName, + Collection: cfg.DatabaseMongo.CollectionName, + } + + application := app.New(cfg.ConnectUrl, cfg.QueueName, databaseMongoConfig, log) go application.AppRabbit.MustRun() // --------------------------- Register stop signal --------------------------- @@ -42,6 +49,7 @@ func main() { ) application.AppRabbit.Close() + application.Storage.Close() //application. diff --git a/cmd/test_main/test_main.go b/cmd/test_main/test_main.go new file mode 100644 index 0000000..f80ea2a --- /dev/null +++ b/cmd/test_main/test_main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/getz-devs/librakeeper-server/internal/searcher-agent/rabbit" +) + +func main() { + _, err := rabbit.ScrapISBNFindBook("9785206000344") + if err != nil { + panic(err) + } +} diff --git a/config/searcher-agent/local.yaml.example b/config/searcher-agent/local.yaml.example index 8a15a3c..8cb9585 100644 --- a/config/searcher-agent/local.yaml.example +++ b/config/searcher-agent/local.yaml.example @@ -1,4 +1,9 @@ env: "local" queue_name: "searcher" -connect_url: "amqp://guest:guest@192.168.1.161:5672/" \ No newline at end of file +connect_url: "amqp://guest:guest@192.168.1.161:5672/" + +database_mongo: + connect_url: "mongodb://192.168.1.199:27017/" + database_name: "TempData" + collection_name_books: "books" \ No newline at end of file diff --git a/go.mod b/go.mod index e0ddcd0..87c4bcf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/getz-devs/librakeeper-server go 1.22 require ( - github.com/getz-devs/librakeeper-protos v0.0.2 + github.com/getz-devs/librakeeper-protos v0.0.3 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 github.com/ilyakaznacheev/cleanenv v1.5.0 go.mongodb.org/mongo-driver v1.8.3 diff --git a/go.sum b/go.sum index c6cad5b..c1c1cf3 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/getz-devs/librakeeper-protos v0.0.1 h1:2iWkQEV2AwCyZJDGlFObYWGHAD+6GT github.com/getz-devs/librakeeper-protos v0.0.1/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= github.com/getz-devs/librakeeper-protos v0.0.2 h1:NbFBVwR5nx1b5BIxx5epKNePEdMcn5CBUN6kF1bgAa0= github.com/getz-devs/librakeeper-protos v0.0.2/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= +github.com/getz-devs/librakeeper-protos v0.0.3 h1:HUOqfHA/UVNL6EnDOVUeCMniZWY1z4pCW4RCBnFPdlU= +github.com/getz-devs/librakeeper-protos v0.0.3/go.mod h1:WJD3/q0XfM1zEktjnClClyki3zTms5+uhghADvE8X4Q= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= diff --git a/internal/searcher-agent/app/app.go b/internal/searcher-agent/app/app.go index 211ecfe..33bdd7d 100644 --- a/internal/searcher-agent/app/app.go +++ b/internal/searcher-agent/app/app.go @@ -2,16 +2,28 @@ package app import ( app_rabbit "github.com/getz-devs/librakeeper-server/internal/searcher-agent/app/rabbit" + mongostorage "github.com/getz-devs/librakeeper-server/internal/searcher-agent/storage/mongo" "log/slog" ) type App struct { AppRabbit *app_rabbit.RabbitApp + Storage *mongostorage.Storage } -func New(rabbitUrl string, queueName string, log *slog.Logger) *App { - appRabbit := app_rabbit.New(rabbitUrl, queueName, log) +func New( + rabbitUrl string, + queueName string, + databaseMongoConfig mongostorage.DatabaseMongoConfig, + log *slog.Logger, +) *App { + + storage := mongostorage.New(databaseMongoConfig) + + appRabbit := app_rabbit.New(rabbitUrl, queueName, log, storage) + return &App{ AppRabbit: appRabbit, + Storage: storage, } } diff --git a/internal/searcher-agent/app/rabbit/app_rabbit.go b/internal/searcher-agent/app/rabbit/app_rabbit.go index 3ad3537..68ca513 100644 --- a/internal/searcher-agent/app/rabbit/app_rabbit.go +++ b/internal/searcher-agent/app/rabbit/app_rabbit.go @@ -1,6 +1,7 @@ package app_rabbit import ( + "context" "fmt" "github.com/getz-devs/librakeeper-server/internal/searcher-agent/rabbit" amqp "github.com/rabbitmq/amqp091-go" @@ -12,6 +13,7 @@ type RabbitApp struct { channel *amqp.Channel msgs <-chan amqp.Delivery log *slog.Logger + handler *rabbit.Handler } // const rabbitUrl = "amqp://guest:guest@localhost:5672/" @@ -22,7 +24,7 @@ func failOnError(err error, msg string) { } } -func New(rabbitUrl string, queueName string, log *slog.Logger) *RabbitApp { +func New(rabbitUrl string, queueName string, log *slog.Logger, requestStorage rabbit.RequestStorage) *RabbitApp { const op = "rabbitmq.RabbitApp.New" log = log.With(slog.String("op", op)) @@ -54,11 +56,17 @@ func New(rabbitUrl string, queueName string, log *slog.Logger) *RabbitApp { log = log.With("queue", q.Name) log.Info("Connected to RabbitMQ") + + // Handler create + + handler := rabbit.New(log, requestStorage) + return &RabbitApp{ connection: conn, channel: ch, log: log, msgs: msgs, + handler: handler, } } @@ -74,8 +82,8 @@ func (r *RabbitApp) Run() error { for d := range r.msgs { logger := r.log.With(slog.String("messageID", d.MessageId)) logger.Info("Received a message") - if err := rabbit.Handler(d, logger); err != nil { - + ctx := context.TODO() + if err := r.handler.Handle(ctx, d); err != nil { logger.Error("Failed to parse message", err) err := d.Nack(false, false) if err != nil { diff --git a/internal/searcher-agent/config/config.go b/internal/searcher-agent/config/config.go index de13140..b13ee65 100644 --- a/internal/searcher-agent/config/config.go +++ b/internal/searcher-agent/config/config.go @@ -11,6 +11,14 @@ type Config struct { QueueName string `yaml:"queue_name" env:"QUEUE_NAME" env-default:"searcher"` ConnectUrl string `yaml:"connect_url" env:"CONNECT_URL" env-required:"true"` + + DatabaseMongo DatabaseMongoConfig `yaml:"database_mongo"` +} + +type DatabaseMongoConfig struct { + ConnectURL string `yaml:"connect_url" env-required:"true"` + DatabaseName string `yaml:"database_name" env-required:"true"` + CollectionName string `yaml:"collection_name_books" env-required:"true"` } func MustLoad() *Config { @@ -44,10 +52,5 @@ func fetchConfigPath() string { if res == "" { res = os.Getenv("CONFIG_PATH") } - - //if res == "" { - // res = "config.yml" - //} - return res } diff --git a/internal/searcher-agent/rabbit/rabbit.go b/internal/searcher-agent/rabbit/rabbit.go index 474152a..b82c7ce 100644 --- a/internal/searcher-agent/rabbit/rabbit.go +++ b/internal/searcher-agent/rabbit/rabbit.go @@ -1,6 +1,7 @@ package rabbit import ( + "context" "fmt" "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" rabbitDefines "github.com/getz-devs/librakeeper-server/lib/rabbit/getz.rabbitProto.v1" @@ -10,42 +11,57 @@ import ( "log/slog" ) -//type Parser struct { -// log *slog.Logger -//} -// -//func New(log *slog.Logger) *Parser { -// const op -// return &Parser{ -// log: log, -// } -//} - -func Handler(delivery amqp.Delivery, log *slog.Logger) error { - const op = "rabbit.Handler" - log = log.With(slog.String("op", op)) +type Handler struct { + log *slog.Logger + requestStorage RequestStorage +} + +func New(log *slog.Logger, requestStorage RequestStorage) *Handler { + return &Handler{ + log: log, + requestStorage: requestStorage, + } +} + +type RequestStorage interface { + CompleteRequest(ctx context.Context, isbn string, books []*bookModels.BookInShop) error + RejectRequest(ctx context.Context, isbn string) error +} + +func (h *Handler) Handle(ctx context.Context, delivery amqp.Delivery) error { + const op = "rabbit.Handler.Handle" + log := h.log.With(slog.String("op", op)) msg := &rabbitDefines.ISBNMessage{} - err := proto.Unmarshal(delivery.Body, msg) - if err != nil { + if err := proto.Unmarshal(delivery.Body, msg); err != nil { + log.Error("Error unmarshaling", err) return err } - err = scrapISBNFindBook(msg.GetIsbn()) + books, err := ScrapISBNFindBook(msg.GetIsbn()) if err != nil { + if err := h.requestStorage.RejectRequest(ctx, msg.GetIsbn()); err != nil { + log.Error("Error rejecting request", err) + } + return err + } + + if err := h.requestStorage.CompleteRequest(ctx, msg.GetIsbn(), books); err != nil { + log.Error("Error completing request", err) return err } return nil } -const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s" +const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s&r=0&s=1&viewsize=15&startidx=0" -func scrapISBNFindBook(isbn string) error { +func ScrapISBNFindBook(isbn string) ([]*bookModels.BookInShop, error) { var books []*bookModels.BookInShop c := colly.NewCollector() - c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (HTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" + c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" + c.OnHTML( "section.container.results", func(e *colly.HTMLElement) { @@ -66,11 +82,11 @@ func scrapISBNFindBook(isbn string) error { preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) err := c.Visit(preparedUrl) if err != nil { - return err + return nil, err } for _, p := range books { fmt.Printf("%+v\n\n", p) } - return nil + return books, nil } diff --git a/internal/searcher-agent/storage/mongo/mongo.go b/internal/searcher-agent/storage/mongo/mongo.go new file mode 100644 index 0000000..b9ef9af --- /dev/null +++ b/internal/searcher-agent/storage/mongo/mongo.go @@ -0,0 +1,82 @@ +package mongostorage + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Storage struct { + client *mongo.Client + col *mongo.Collection +} + +type DatabaseMongoConfig struct { + ConnectUrl string + Database string + Collection string +} + +func New(databaseConfig DatabaseMongoConfig) *Storage { + client, err := mongo.Connect(context.TODO(), options.Client(). + ApplyURI(databaseConfig.ConnectUrl)) + if err != nil { + panic(err) + } + + coll := client.Database(databaseConfig.Database).Collection(databaseConfig.Collection) + + return &Storage{ + client: client, + col: coll, + } +} + +func (s *Storage) Close() { + if err := s.client.Disconnect(context.TODO()); err != nil { + panic(err) + } +} + +func (s *Storage) FindOrCreateRequest(ctx context.Context, isbn string) (bookModels.SearchRequest, bool, error) { + // insert if not exist (upsert) + filter := bson.D{{"isbn", isbn}} + insertValue := bookModels.New(isbn) + value := bson.D{{"$setOnInsert", insertValue}} + opts := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After) + + var result bookModels.SearchRequest + err := s.col.FindOneAndUpdate(ctx, filter, value, opts).Decode(&result) + if err != nil { + return bookModels.SearchRequest{}, false, err + } + if insertValue.ID == result.ID { + return bookModels.SearchRequest{}, true, nil + } + return result, false, nil +} + +func (s *Storage) CompleteRequest(ctx context.Context, isbn string, books []*bookModels.BookInShop) error { + filter := bson.D{{"isbn", isbn}} + values := bson.D{{"$set", bson.D{ + {"books", books}, + {"status", bookModels.Success}, + }}} + if _, err := s.col.UpdateOne(ctx, filter, values); err != nil { + return err + } + return nil +} + +func (s *Storage) RejectRequest(ctx context.Context, isbn string) error { + filter := bson.D{{"isbn", isbn}} + values := bson.D{{"$set", bson.D{ + {"status", bookModels.Failed}, + }}} + if _, err := s.col.UpdateOne(ctx, filter, values); err != nil { + return err + } + return nil +} diff --git a/internal/searcher/grpc/server.go b/internal/searcher/grpc/server.go index 2083751..66698d3 100644 --- a/internal/searcher/grpc/server.go +++ b/internal/searcher/grpc/server.go @@ -3,6 +3,7 @@ package searcher import ( "context" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/searcher-shared/domain/bookModels" searcherservice "github.com/getz-devs/librakeeper-server/internal/searcher/services/searcher" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -33,8 +34,32 @@ func (s *serverAPI) SearchByISBN( } s.log.Info("Results", slog.Any("results", results)) + var books []*searcherv1.Book + if len(results.Books) >= 0 { + for _, book := range results.Books { + books = append(books, &searcherv1.Book{ + Title: book.Title, + Author: book.Author, + Publishing: book.Publishing, + ImgUrl: book.ImgUrl, + ShopName: book.ShopName, + }) + } + } + + statusReturn := searcherv1.SearchByISBNResponse_PROCESSING + switch results.Status { + case bookModels.Pending: + statusReturn = searcherv1.SearchByISBNResponse_PROCESSING + case bookModels.Failed: + statusReturn = searcherv1.SearchByISBNResponse_PROCESSING + case bookModels.Success: + statusReturn = searcherv1.SearchByISBNResponse_SUCCESS + default: + statusReturn = searcherv1.SearchByISBNResponse_PROCESSING + } return &searcherv1.SearchByISBNResponse{ - Status: searcherv1.SearchByISBNResponse_SUCCESS, - Books: []*searcherv1.Book{}, + Status: statusReturn, + Books: books, }, nil } diff --git a/internal/searcher/storage/mongo/mongo.go b/internal/searcher/storage/mongo/mongo.go index 5a256a7..b6d47bf 100644 --- a/internal/searcher/storage/mongo/mongo.go +++ b/internal/searcher/storage/mongo/mongo.go @@ -40,48 +40,6 @@ func (s *Storage) Close() { } } -//func (s *Storage) SearchByISBN(ctx context.Context, isbn string) (*models.BooksSearchResult, error) { -// const op = "storage.mongo.FindByISBN" -// -// // Pass these options to the Find method -// findOptions := options.Find() -// findOptions.SetLimit(5) -// -// // Here's an array in which you can store the decoded documents -// var results models.BooksSearchResult -// -// // Passing bson.D{{}} as the filter matches all documents in the collection -// cur, err := s.col.Find(ctx, bson.D{{}}, findOptions) -// if err != nil { -// log.Fatal(err) -// } -// -// // Finding multiple documents returns a cursor -// // Iterating through the cursor allows us to decode documents one at a time -// for cur.Next(ctx) { -// -// // create a value into which the single document can be decoded -// var elem models.BookSearchResult -// err := cur.Decode(&elem) -// if err != nil { -// log.Fatal(err) -// } -// -// results = append(results, &elem) -// } -// -// if err := cur.Err(); err != nil { -// return nil, fmt.Errorf("%s ,error when reading cursor: %w", op, err) -// } -// -// // Close the cursor once finished -// if err := cur.Close(ctx); err != nil { -// return nil, fmt.Errorf("%s ,error when closing cursor: %w", op, err) -// } -// -// return &results, nil -//} - func (s *Storage) FindOrCreateRequest(ctx context.Context, isbn string) (bookModels.SearchRequest, bool, error) { // insert if not exist (upsert) filter := bson.D{{"isbn", isbn}} From 0b47f53db4eb61eed59b9af74a39f7777f7fd85d Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:05:50 +0300 Subject: [PATCH 23/56] moved config.example.yaml changed cmd main.go --- cmd/server/main.go | 21 +-------------------- {cmd => config}/server/config.example.yaml | 0 2 files changed, 1 insertion(+), 20 deletions(-) rename {cmd => config}/server/config.example.yaml (100%) diff --git a/cmd/server/main.go b/cmd/server/main.go index 224fb76..3902c06 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,7 +11,7 @@ import ( func main() { cfg := config.MustLoad() - log := setupLogger(cfg.Env) + log := prettylog.SetupLogger(cfg.Env) log.Info("starting librakeeper srv", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) // Create and initialize the srv @@ -27,22 +27,3 @@ func main() { os.Exit(1) } } - -func setupLogger(env string) *slog.Logger { - var log *slog.Logger - - switch env { - case "local": - log = slog.New(prettylog.NewHandler(&slog.HandlerOptions{ - Level: slog.LevelDebug, - AddSource: false, - ReplaceAttr: nil, - })) - case "development": - log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) - case "production": - log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) - } - - return log -} diff --git a/cmd/server/config.example.yaml b/config/server/config.example.yaml similarity index 100% rename from cmd/server/config.example.yaml rename to config/server/config.example.yaml From 374b9ef8cb229f9dbde162de091824bef7472ce5 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:36:39 +0300 Subject: [PATCH 24/56] added compiled rabbit proto --- lib/rabbit/getz.rabbitProto.v1/rabbit.pb.go | 143 ++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 lib/rabbit/getz.rabbitProto.v1/rabbit.pb.go diff --git a/lib/rabbit/getz.rabbitProto.v1/rabbit.pb.go b/lib/rabbit/getz.rabbitProto.v1/rabbit.pb.go new file mode 100644 index 0000000..a873394 --- /dev/null +++ b/lib/rabbit/getz.rabbitProto.v1/rabbit.pb.go @@ -0,0 +1,143 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v5.26.1 +// source: rabbit.proto + +package rabbitDefines + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ISBNMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Isbn string `protobuf:"bytes,1,opt,name=isbn,proto3" json:"isbn,omitempty"` +} + +func (x *ISBNMessage) Reset() { + *x = ISBNMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_rabbit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ISBNMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ISBNMessage) ProtoMessage() {} + +func (x *ISBNMessage) ProtoReflect() protoreflect.Message { + mi := &file_rabbit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ISBNMessage.ProtoReflect.Descriptor instead. +func (*ISBNMessage) Descriptor() ([]byte, []int) { + return file_rabbit_proto_rawDescGZIP(), []int{0} +} + +func (x *ISBNMessage) GetIsbn() string { + if x != nil { + return x.Isbn + } + return "" +} + +var File_rabbit_proto protoreflect.FileDescriptor + +var file_rabbit_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x72, 0x61, 0x62, 0x62, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, + 0x70, 0x62, 0x22, 0x21, 0x0a, 0x0b, 0x49, 0x53, 0x42, 0x4e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x73, 0x62, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x69, 0x73, 0x62, 0x6e, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x65, 0x74, 0x7a, 0x2e, 0x72, 0x61, + 0x62, 0x62, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x3b, 0x72, 0x61, 0x62, + 0x62, 0x69, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_rabbit_proto_rawDescOnce sync.Once + file_rabbit_proto_rawDescData = file_rabbit_proto_rawDesc +) + +func file_rabbit_proto_rawDescGZIP() []byte { + file_rabbit_proto_rawDescOnce.Do(func() { + file_rabbit_proto_rawDescData = protoimpl.X.CompressGZIP(file_rabbit_proto_rawDescData) + }) + return file_rabbit_proto_rawDescData +} + +var file_rabbit_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_rabbit_proto_goTypes = []any{ + (*ISBNMessage)(nil), // 0: pb.ISBNMessage +} +var file_rabbit_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_rabbit_proto_init() } +func file_rabbit_proto_init() { + if File_rabbit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rabbit_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*ISBNMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rabbit_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_rabbit_proto_goTypes, + DependencyIndexes: file_rabbit_proto_depIdxs, + MessageInfos: file_rabbit_proto_msgTypes, + }.Build() + File_rabbit_proto = out.File + file_rabbit_proto_rawDesc = nil + file_rabbit_proto_goTypes = nil + file_rabbit_proto_depIdxs = nil +} From 2331ef0bc588006c0d06828a62fe420748954bfc Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:57:08 +0300 Subject: [PATCH 25/56] FIX: parser fixed and improved! --- internal/searcher-agent/rabbit/rabbit.go | 51 ++++++++++++++++--- .../searcher-agent/storage/mongo/mongo.go | 3 ++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/internal/searcher-agent/rabbit/rabbit.go b/internal/searcher-agent/rabbit/rabbit.go index b82c7ce..dfa0540 100644 --- a/internal/searcher-agent/rabbit/rabbit.go +++ b/internal/searcher-agent/rabbit/rabbit.go @@ -9,6 +9,7 @@ import ( "github.com/golang/protobuf/proto" amqp "github.com/rabbitmq/amqp091-go" "log/slog" + "time" ) type Handler struct { @@ -38,7 +39,7 @@ func (h *Handler) Handle(ctx context.Context, delivery amqp.Delivery) error { return err } - books, err := ScrapISBNFindBook(msg.GetIsbn()) + books, err := h.scrapISBNFindBook(msg.GetIsbn()) if err != nil { if err := h.requestStorage.RejectRequest(ctx, msg.GetIsbn()); err != nil { log.Error("Error rejecting request", err) @@ -56,37 +57,71 @@ func (h *Handler) Handle(ctx context.Context, delivery amqp.Delivery) error { const findBookUrlTemplate = "https://www.findbook.ru/search/d1?isbn=%s&r=0&s=1&viewsize=15&startidx=0" -func ScrapISBNFindBook(isbn string) ([]*bookModels.BookInShop, error) { +func (h *Handler) scrapISBNFindBook(isbn string) ([]*bookModels.BookInShop, error) { + const op = "rabbit.Handler.scrapISBNFindBook" + log := h.log.With(slog.String("op", op), slog.String("isbn", isbn)) + preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) var books []*bookModels.BookInShop - c := colly.NewCollector() + c := colly.NewCollector( + colly.AllowURLRevisit(), + //colly.Async(true), + ) + + //Ignore the robot.txt + c.IgnoreRobotsTxt = true + // Time-out after 20 seconds. + c.SetRequestTimeout(20 * time.Second) + c.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" + retryCount := 0 + maxRetryCount := 3 + + c.OnResponse(func(r *colly.Response) { + // print all headers + if r.Headers.Get("Pragma") == "no-cache" && retryCount < maxRetryCount { + time.Sleep(1 * time.Second) + retryCount++ + r.Request.Visit(preparedUrl) + } + }) + c.OnHTML( "section.container.results", func(e *colly.HTMLElement) { - e.ForEach("div.row.results__line", func(_ int, e *colly.HTMLElement) { book := &bookModels.BookInShop{} err := e.Unmarshal(book) if err != nil { return } + if book.ImgUrl == "/images/camera.png" { + book.ImgUrl = "" + } books = append(books, book) }) - }, ) + c.OnHTML("div.pagination__pages a:has(i.icon-angle-right)", func(e *colly.HTMLElement) { + e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href"))) + }) + c.OnRequest(func(r *colly.Request) { + log.Info("visiting", slog.String("url", r.URL.String())) + }) - preparedUrl := fmt.Sprintf(findBookUrlTemplate, isbn) err := c.Visit(preparedUrl) + c.Wait() if err != nil { return nil, err } - for _, p := range books { - fmt.Printf("%+v\n\n", p) + //for _, p := range books { + // fmt.Printf("%+v\n\n", p) + //} + if len(books) == 0 { + return []*bookModels.BookInShop{}, nil } return books, nil } diff --git a/internal/searcher-agent/storage/mongo/mongo.go b/internal/searcher-agent/storage/mongo/mongo.go index b9ef9af..42b2192 100644 --- a/internal/searcher-agent/storage/mongo/mongo.go +++ b/internal/searcher-agent/storage/mongo/mongo.go @@ -6,6 +6,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "time" ) type Storage struct { @@ -63,6 +64,7 @@ func (s *Storage) CompleteRequest(ctx context.Context, isbn string, books []*boo values := bson.D{{"$set", bson.D{ {"books", books}, {"status", bookModels.Success}, + {"updated_at", time.Now()}, }}} if _, err := s.col.UpdateOne(ctx, filter, values); err != nil { return err @@ -74,6 +76,7 @@ func (s *Storage) RejectRequest(ctx context.Context, isbn string) error { filter := bson.D{{"isbn", isbn}} values := bson.D{{"$set", bson.D{ {"status", bookModels.Failed}, + {"updated_at", time.Now()}, }}} if _, err := s.col.UpdateOne(ctx, filter, values); err != nil { return err From 98477ff30895ef03be7c5381489473a3ebe3acd4 Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:39:58 +0300 Subject: [PATCH 26/56] Feat: Func tests added --- go.mod | 3 ++ internal/searcher/config/config.go | 4 ++ tests/searcher/searcher_test.go | 55 ++++++++++++++++++++++++++++ tests/searcher/suite/suite.go | 59 ++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 tests/searcher/searcher_test.go create mode 100644 tests/searcher/suite/suite.go diff --git a/go.mod b/go.mod index 9e36c97..402e8fb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/rabbitmq/amqp091-go v1.10.0 + github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.8.3 google.golang.org/api v0.187.0 google.golang.org/grpc v1.64.0 @@ -37,6 +38,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -65,6 +67,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/temoto/robotstxt v1.1.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/internal/searcher/config/config.go b/internal/searcher/config/config.go index 9f4a731..4dfc5e2 100644 --- a/internal/searcher/config/config.go +++ b/internal/searcher/config/config.go @@ -41,6 +41,10 @@ func MustLoad() *Config { panic("config path is empty") } + return MustLoadByPath(path) +} + +func MustLoadByPath(path string) *Config { if _, err := os.Stat(path); os.IsNotExist(err) { panic("config file doesn't exist: " + path) } diff --git a/tests/searcher/searcher_test.go b/tests/searcher/searcher_test.go new file mode 100644 index 0000000..978de5f --- /dev/null +++ b/tests/searcher/searcher_test.go @@ -0,0 +1,55 @@ +package tests + +import ( + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/tests/searcher/suite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "math/rand" + "testing" + "time" +) + +const ( + realIsbn = "978-5-4461-2058-1" +) + +func Test1RequestPendingSecondIsFinished(t *testing.T) { + ctx, st := suite.New(t) + + randomIsbn := GenerateRandomIsbn() + response, err := st.SearcherServ.SearchByISBN(ctx, &searcherv1.SearchByISBNRequest{Isbn: randomIsbn}) + require.NoError(t, err) + assert.True(t, response.GetStatus() == searcherv1.SearchByISBNResponse_PROCESSING) + + time.Sleep(5 * time.Second) + response, err = st.SearcherServ.SearchByISBN(ctx, &searcherv1.SearchByISBNRequest{Isbn: randomIsbn}) + require.NoError(t, err) + assert.True(t, response.GetStatus() == searcherv1.SearchByISBNResponse_SUCCESS) +} + +func TestRealIsbn(t *testing.T) { + ctx, st := suite.New(t) + // do-while proccesing + for { + response, err := st.SearcherServ.SearchByISBN(ctx, &searcherv1.SearchByISBNRequest{Isbn: realIsbn}) + require.NoError(t, err) + if response.GetStatus() == searcherv1.SearchByISBNResponse_SUCCESS { + require.NotEmpty(t, response.GetBooks()) + break + } + time.Sleep(2 * time.Second) + } +} + +func GenerateRandomIsbn() string { + // Generate a random 10-digit ISBN + isbn := make([]byte, 10) + for i := 0; i < 9; i++ { + isbn[i] = byte(rand.Intn(10) + '0') + } + isbn[9] = 'X' + + // Convert the byte slice to a string + return string(isbn) +} diff --git a/tests/searcher/suite/suite.go b/tests/searcher/suite/suite.go new file mode 100644 index 0000000..8ad21ec --- /dev/null +++ b/tests/searcher/suite/suite.go @@ -0,0 +1,59 @@ +package suite + +import ( + "context" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/searcher/config" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "net" + "strconv" + + "testing" +) + +type Suite struct { + *testing.T + Cfg *config.Config + SearcherServ searcherv1.SearcherClient +} + +const ( + grpcHost = "localhost" +) + +func New(t *testing.T) (context.Context, *Suite) { + t.Helper() + t.Parallel() + + // TODO: Read test config from env + + cfgManager := config.MustLoadByPath("../config/searcher/local.yaml") + ctx, cancelCtx := context.WithTimeout(context.Background(), cfgManager.GRPC.Timeout) + + t.Cleanup(func() { + t.Helper() + cancelCtx() + }) + + //cc, err := grpc.DialContext(ctx, + // grpcAdress(&cfgManager.GRPC), + // grpc.WithTransportCredentials(insecure.NewCredentials()), + //) + cc, err := grpc.NewClient(grpcAdress(&cfgManager.GRPC), grpc.WithTransportCredentials(insecure.NewCredentials())) + + if err != nil { + t.Fatalf("grpc server connection failed %v", err) + } + + return ctx, &Suite{ + T: t, + Cfg: cfgManager, + SearcherServ: searcherv1.NewSearcherClient(cc), + } + +} + +func grpcAdress(cfg *config.GRPCConfig) string { + return net.JoinHostPort(grpcHost, strconv.Itoa(cfg.Port)) +} From 13e13c4431a7e76cec06c486a8e1c3714830254a Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 3 Jul 2024 17:52:41 +0300 Subject: [PATCH 27/56] improve models and define repositories --- internal/server/models/book.go | 19 +++++++++---------- internal/server/models/bookshelf.go | 11 +++++------ internal/server/models/user.go | 2 +- internal/server/repository/book.go | 16 ++++++++++++++++ internal/server/repository/bookshelf.go | 15 +++++++++++++++ internal/server/repository/user.go | 14 ++++++++++++++ 6 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 internal/server/repository/book.go create mode 100644 internal/server/repository/bookshelf.go create mode 100644 internal/server/repository/user.go diff --git a/internal/server/models/book.go b/internal/server/models/book.go index d23cc82..0f48cd2 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -2,21 +2,20 @@ package models import ( "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" "time" ) // Book represents a book in the library. type Book struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` - BookshelfID primitive.ObjectID `bson:"bookshelf_id" json:"bookshelf_id"` - Title string `bson:"title" json:"title"` - Author string `bson:"author" json:"author"` - ISBN string `bson:"isbn" json:"isbn"` - Description string `bson:"description" json:"description"` - CoverImage string `bson:"cover_image" json:"cover_image"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + ID string `bson:"_id,omitempty" json:"id"` + BookshelfID string `bson:"bookshelf_id" json:"bookshelf_id"` + Title string `bson:"title" json:"title"` + Author string `bson:"author" json:"author"` + ISBN string `bson:"isbn" json:"isbn"` + Description string `bson:"description" json:"description"` + CoverImage string `bson:"cover_image" json:"cover_image"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } // ToMap converts the Book struct to a bson.M map for MongoDB updates. diff --git a/internal/server/models/bookshelf.go b/internal/server/models/bookshelf.go index c4f4461..45d1cce 100644 --- a/internal/server/models/bookshelf.go +++ b/internal/server/models/bookshelf.go @@ -2,17 +2,16 @@ package models import ( "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" "time" ) // Bookshelf represents a collection of books. type Bookshelf struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` - UserID string `bson:"user_id" json:"user_id"` - Name string `bson:"name" json:"name"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + ID string `bson:"_id,omitempty" json:"id"` + UserID string `bson:"user_id" json:"user_id"` + Name string `bson:"name" json:"name"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } // ToMap converts the Bookshelf struct to a bson.M map for MongoDB updates. diff --git a/internal/server/models/user.go b/internal/server/models/user.go index a50b246..22cc454 100644 --- a/internal/server/models/user.go +++ b/internal/server/models/user.go @@ -7,7 +7,7 @@ import ( // User represents a user in the system. type User struct { - ID string `bson:"_id" json:"id"` // Firebase UID as primary key + ID string `bson:"_id,omitempty" json:"id"` // Firebase UID as primary key DisplayName string `bson:"display_name" json:"display_name"` CreatedAt time.Time `bson:"created_at" json:"created_at"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` diff --git a/internal/server/repository/book.go b/internal/server/repository/book.go new file mode 100644 index 0000000..6469108 --- /dev/null +++ b/internal/server/repository/book.go @@ -0,0 +1,16 @@ +package repository + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/models" +) + +// BookRepo defines the interface for book repository operations. +type BookRepo interface { + Create(ctx context.Context, book *models.Book) error + GetByID(ctx context.Context, id string) (*models.Book, error) + GetByUserID(ctx context.Context, id string, page int64, limit int64) ([]*models.Book, error) + GetByBookshelfID(ctx context.Context, id string, page int64, limit int64) ([]*models.Book, error) + Update(ctx context.Context, id string, update *models.Book) error + Delete(ctx context.Context, id string) error +} diff --git a/internal/server/repository/bookshelf.go b/internal/server/repository/bookshelf.go new file mode 100644 index 0000000..30e94f9 --- /dev/null +++ b/internal/server/repository/bookshelf.go @@ -0,0 +1,15 @@ +package repository + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/models" +) + +// BookshelfRepo defines the interface for bookshelf repository operations. +type BookshelfRepo interface { + Create(ctx context.Context, bookshelf *models.Bookshelf) error + GetByID(ctx context.Context, id string) (*models.Bookshelf, error) + GetByUserID(ctx context.Context, id string, page int64, limit int64) ([]*models.Bookshelf, error) + Update(ctx context.Context, id string, update *models.Bookshelf) error + Delete(ctx context.Context, id string) error +} diff --git a/internal/server/repository/user.go b/internal/server/repository/user.go new file mode 100644 index 0000000..12160d3 --- /dev/null +++ b/internal/server/repository/user.go @@ -0,0 +1,14 @@ +package repository + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/models" +) + +// UserRepo defines the interface for user repository operations. +type UserRepo interface { + Create(ctx context.Context, user *models.User) error + GetByID(ctx context.Context, id string) (*models.User, error) + Update(ctx context.Context, id string, update *models.User) error + Delete(ctx context.Context, id string) error +} From 3b46f9df8066fba81fd99baea5c07ff559143add Mon Sep 17 00:00:00 2001 From: Sergeydigl3 <26508358+Sergeydigl3@users.noreply.github.com> Date: Thu, 4 Jul 2024 04:33:48 +0300 Subject: [PATCH 28/56] Feat: Base docker files finished --- config/searcher-agent/docker-local.yaml | 9 +++ config/searcher/docker-local.yaml | 13 ++++ config/server/docker-local.yaml | 14 +++++ docker/.dockerignore | 38 ++++++++++++ docker/Dockerfile.searcher | 31 ++++++++++ docker/Dockerfile.searcher-agent | 29 +++++++++ docker/Dockerfile.server | 30 +++++++++ docker/docker-compose.yaml | 82 +++++++++++++++++++++++++ internal/searcher/config/config.go | 2 - 9 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 config/searcher-agent/docker-local.yaml create mode 100644 config/searcher/docker-local.yaml create mode 100644 config/server/docker-local.yaml create mode 100644 docker/.dockerignore create mode 100644 docker/Dockerfile.searcher create mode 100644 docker/Dockerfile.searcher-agent create mode 100644 docker/Dockerfile.server create mode 100644 docker/docker-compose.yaml diff --git a/config/searcher-agent/docker-local.yaml b/config/searcher-agent/docker-local.yaml new file mode 100644 index 0000000..d23b8eb --- /dev/null +++ b/config/searcher-agent/docker-local.yaml @@ -0,0 +1,9 @@ +env: "local" + +queue_name: "searcher" +connect_url: "amqp://test:test@rabbitmq/" + +database_mongo: + connect_url: "mongodb://mongodb/" + database_name: "docker_searcher" + collection_name_books: "books" \ No newline at end of file diff --git a/config/searcher/docker-local.yaml b/config/searcher/docker-local.yaml new file mode 100644 index 0000000..694170d --- /dev/null +++ b/config/searcher/docker-local.yaml @@ -0,0 +1,13 @@ +env: "local" + +grpc: + port: 8081 + +database_mongo: + connect_url: "mongodb://mongodb/" + database_name: "docker_searcher" + collection_name_books: "books" + +rabbit: + url: "amqp://test:test@rabbitmq/" + queue_name: "searcher" \ No newline at end of file diff --git a/config/server/docker-local.yaml b/config/server/docker-local.yaml new file mode 100644 index 0000000..96b09d3 --- /dev/null +++ b/config/server/docker-local.yaml @@ -0,0 +1,14 @@ +env: "local" + +server: + port: 8080 + allowed_origins: + - https://* + - http://* + +database: + uri: mongodb://mongo:27017 + name: docker_server + +auth: + config_path: /config/secret.json diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..17159ec --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,38 @@ +# Исключаем сборочные артефакты +**/*.exe +**/*.exe~ +**/*.dll +**/*.so +**/*.dylib +**/*.test +**/*.out + +# Исключаем временные файлы редактора +**/*~ +**/*.swp +**/*.swo +**/*.swn +*.log + +# Исключаем директории +.git +.gitignore +node_modules +vendor + +# Исключаем файлы конфигурации и кэш +.env +*.env +.cache + +# Исключаем Dockerfile, docker-compose.yml и все внутри docker, чтобы избежать копирования самого файла .dockerignore +docker/ +Dockerfile* + +# Исключаем бинарные файлы, если они есть в корне +searcher +searcher-agent +server + +# Исключаем файлы тестов +tests/ \ No newline at end of file diff --git a/docker/Dockerfile.searcher b/docker/Dockerfile.searcher new file mode 100644 index 0000000..b15aab3 --- /dev/null +++ b/docker/Dockerfile.searcher @@ -0,0 +1,31 @@ +# Используем официальный образ Golang +FROM golang:1.22-alpine AS builder + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /app + +# Копируем весь проект (вверх на два уровня от текущей директории docker) +COPY ../.. . + +# Загружаем зависимости +RUN go mod download + +# Сборка бинарника для searcher +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o searcher cmd/searcher/main.go + +# Создаем минимальный образ для запуска +FROM alpine:latest + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /root/ + +# Копируем собранный бинарник из этапа сборки +COPY --from=builder /app/searcher . + +# Копируем конфигурационные файлы +COPY --from=builder /app/config/searcher /config + +EXPOSE 8081 + +# Устанавливаем команду по умолчанию для запуска +CMD ["./searcher"] diff --git a/docker/Dockerfile.searcher-agent b/docker/Dockerfile.searcher-agent new file mode 100644 index 0000000..3235a15 --- /dev/null +++ b/docker/Dockerfile.searcher-agent @@ -0,0 +1,29 @@ +# Используем официальный образ Golang +FROM golang:1.22-alpine AS builder + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /app + +# Копируем весь проект (вверх на два уровня от текущей директории docker) +COPY ../.. . + +# Загружаем зависимости +RUN go mod download + +# Сборка бинарника для searcher-agent +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o searcher-agent cmd/searcher-agent/main.go + +# Создаем минимальный образ для запуска +FROM alpine:latest + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /root/ + +# Копируем собранный бинарник из этапа сборки +COPY --from=builder /app/searcher-agent . + +# Копируем конфигурационные файлы +COPY --from=builder /app/config/searcher-agent /config + +# Устанавливаем команду по умолчанию для запуска +CMD ["./searcher-agent"] diff --git a/docker/Dockerfile.server b/docker/Dockerfile.server new file mode 100644 index 0000000..ca8146c --- /dev/null +++ b/docker/Dockerfile.server @@ -0,0 +1,30 @@ +# Используем официальный образ Golang +FROM golang:1.22-alpine AS builder + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /app + +# Копируем весь проект (вверх на два уровня от текущей директории docker) +COPY ../.. . + +# Загружаем зависимости +RUN go mod download + +# Сборка бинарника для server +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server cmd/server/main.go + +# Создаем минимальный образ для запуска +FROM alpine:latest + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /root/ + +# Копируем собранный бинарник из этапа сборки +COPY --from=builder /app/server . + +# Копируем конфигурационные файлы +COPY --from=builder /app/config/server /config + +EXPOSE 8080 +# Устанавливаем команду по умолчанию для запуска +CMD ["./server"] diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..2512ce6 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,82 @@ +services: + searcher: + build: + context: .. + dockerfile: docker/Dockerfile.searcher + volumes: + - ../config/searcher:/config + ports: + - "8081:8081" + environment: + - CONFIG_PATH=/config/docker-local.yaml + depends_on: + mongodb: + condition: service_healthy + rabbitmq: + condition: service_healthy + restart: unless-stopped + + searcher-agent: + build: + context: .. + dockerfile: docker/Dockerfile.searcher-agent + volumes: + - ../config/searcher-agent:/config + environment: + - CONFIG_PATH=/config/docker-local.yaml + depends_on: + mongodb: + condition: service_healthy + rabbitmq: + condition: service_healthy + restart: unless-stopped + + + server: + build: + context: .. + dockerfile: docker/Dockerfile.server + volumes: + - ../config/server:/config + ports: + - "8080:8080" + environment: + - CONFIG_PATH=/config/docker-local.yaml + depends_on: + mongodb: + condition: service_healthy + rabbitmq: + condition: service_healthy + searcher: + condition: service_started + + mongodb: + image: mongo:latest + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + start_period: 5s + + rabbitmq: + image: rabbitmq:management + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=test + - RABBITMQ_DEFAULT_PASS=test + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 10s + timeout: 10s + retries: 5 + start_period: 10s + +volumes: + mongo-data: \ No newline at end of file diff --git a/internal/searcher/config/config.go b/internal/searcher/config/config.go index 4dfc5e2..767ac34 100644 --- a/internal/searcher/config/config.go +++ b/internal/searcher/config/config.go @@ -10,8 +10,6 @@ import ( type Config struct { Env string `yaml:"env" env-default:"local"` - StoragePath string `yaml:"storage_path" env-required:"true"` - GRPC GRPCConfig `yaml:"grpc"` DatabaseMongo DatabaseMongoConfig `yaml:"database_mongo"` From c99784265e4a255b8279eac6e9c838363f373c2c Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 01:07:09 +0300 Subject: [PATCH 29/56] improve Update methods --- internal/server/models/book.go | 9 +++++++++ internal/server/models/bookshelf.go | 5 +++++ internal/server/models/user.go | 5 +++++ internal/server/repository/book.go | 6 +++--- internal/server/repository/bookshelf.go | 2 +- internal/server/repository/user.go | 2 +- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/internal/server/models/book.go b/internal/server/models/book.go index 0f48cd2..19240f3 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -31,3 +31,12 @@ func (b *Book) ToMap() bson.M { "updated_at": b.UpdatedAt, } } + +// BookUpdate represents fields that can be updated in a Book. +type BookUpdate struct { + Title *string `bson:"title,omitempty" json:"title,omitempty"` // Optional fields for update + Author *string `bson:"author,omitempty" json:"author,omitempty"` + ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` + Description *string `bson:"description,omitempty" json:"description,omitempty"` + CoverImage *string `bson:"cover_image,omitempty" json:"cover_image,omitempty"` +} diff --git a/internal/server/models/bookshelf.go b/internal/server/models/bookshelf.go index 45d1cce..a30da21 100644 --- a/internal/server/models/bookshelf.go +++ b/internal/server/models/bookshelf.go @@ -23,3 +23,8 @@ func (b *Bookshelf) ToMap() bson.M { "updated_at": b.UpdatedAt, } } + +// BookshelfUpdate represents fields that can be updated in a Bookshelf. +type BookshelfUpdate struct { + Name *string `bson:"name,omitempty" json:"name,omitempty"` // Optional field for update +} diff --git a/internal/server/models/user.go b/internal/server/models/user.go index 22cc454..f782caa 100644 --- a/internal/server/models/user.go +++ b/internal/server/models/user.go @@ -20,3 +20,8 @@ func (u *User) ToMap() bson.M { "updated_at": u.UpdatedAt, } } + +// UserUpdate represents fields that can be updated in a User. +type UserUpdate struct { + DisplayName *string `bson:"display_name,omitempty" json:"display_name,omitempty"` // Optional field for update +} diff --git a/internal/server/repository/book.go b/internal/server/repository/book.go index 6469108..8840e56 100644 --- a/internal/server/repository/book.go +++ b/internal/server/repository/book.go @@ -9,8 +9,8 @@ import ( type BookRepo interface { Create(ctx context.Context, book *models.Book) error GetByID(ctx context.Context, id string) (*models.Book, error) - GetByUserID(ctx context.Context, id string, page int64, limit int64) ([]*models.Book, error) - GetByBookshelfID(ctx context.Context, id string, page int64, limit int64) ([]*models.Book, error) - Update(ctx context.Context, id string, update *models.Book) error + GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) + GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) + Update(ctx context.Context, id string, update *models.BookUpdate) error Delete(ctx context.Context, id string) error } diff --git a/internal/server/repository/bookshelf.go b/internal/server/repository/bookshelf.go index 30e94f9..68f233d 100644 --- a/internal/server/repository/bookshelf.go +++ b/internal/server/repository/bookshelf.go @@ -10,6 +10,6 @@ type BookshelfRepo interface { Create(ctx context.Context, bookshelf *models.Bookshelf) error GetByID(ctx context.Context, id string) (*models.Bookshelf, error) GetByUserID(ctx context.Context, id string, page int64, limit int64) ([]*models.Bookshelf, error) - Update(ctx context.Context, id string, update *models.Bookshelf) error + Update(ctx context.Context, id string, update *models.BookshelfUpdate) error Delete(ctx context.Context, id string) error } diff --git a/internal/server/repository/user.go b/internal/server/repository/user.go index 12160d3..7034542 100644 --- a/internal/server/repository/user.go +++ b/internal/server/repository/user.go @@ -9,6 +9,6 @@ import ( type UserRepo interface { Create(ctx context.Context, user *models.User) error GetByID(ctx context.Context, id string) (*models.User, error) - Update(ctx context.Context, id string, update *models.User) error + Update(ctx context.Context, id string, update *models.UserUpdate) error Delete(ctx context.Context, id string) error } From 0548d57d44cc72e1a1ecfc1bf783e49eb8f2596e Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 02:07:30 +0300 Subject: [PATCH 30/56] implement Mongo BookRepo --- internal/server/storage/mongo/book.go | 170 ++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 internal/server/storage/mongo/book.go diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go new file mode 100644 index 0000000..b78ae89 --- /dev/null +++ b/internal/server/storage/mongo/book.go @@ -0,0 +1,170 @@ +package mongo + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log/slog" + "time" +) + +// ErrBookNotFound occurs when a book is not found in the database. +var ErrBookNotFound = errors.New("book not found") + +// ErrBookAlreadyExists occurs when trying to create a book with an ID that already exists. +var ErrBookAlreadyExists = errors.New("book already exists") + +// BookRepo implements the repository.BookRepo interface for MongoDB. +type BookRepo struct { + collection *mongo.Collection + log *slog.Logger +} + +// NewBookRepo creates a new BookRepo instance. +func NewBookRepo(db *mongo.Database, log *slog.Logger) repository.BookRepo { + return &BookRepo{ + collection: db.Collection("books"), // Note: Collection name corrected to "books" + log: log, + } +} + +// Create inserts a new book into the database. +func (r *BookRepo) Create(ctx context.Context, book *models.Book) error { + book.ID = primitive.NewObjectID().Hex() + book.CreatedAt = time.Now() + book.UpdatedAt = time.Now() + + _, err := r.collection.InsertOne(ctx, book) + if err != nil { + // Check for duplicate key error + var writeErr mongo.WriteException + if errors.As(err, &writeErr) && writeErr.WriteErrors[0].Code == 11000 { + return ErrBookAlreadyExists + } + + r.log.Error("failed to create book", slog.Any("error", err)) + return fmt.Errorf("failed to create book: %w", err) + } + + return nil +} + +// GetByID retrieves a book from the database by its ID. +func (r *BookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, fmt.Errorf("invalid book ID: %w", err) + } + + var book models.Book + err = r.collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&book) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrBookNotFound + } + return nil, fmt.Errorf("failed to get book: %w", err) + } + return &book, nil +} + +// GetByUserID retrieves books associated with a specific user ID. +func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { + objectUserID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return nil, fmt.Errorf("invalid user ID: %w", err) + } + + findOptions := options.Find() + findOptions.SetSkip((page - 1) * limit) + findOptions.SetLimit(limit) + + cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) + if err != nil { + return nil, fmt.Errorf("failed to get books by user ID: %w", err) + } + defer cursor.Close(ctx) + + var books []*models.Book + if err = cursor.All(ctx, &books); err != nil { + return nil, fmt.Errorf("failed to decode books: %w", err) + } + + return books, nil +} + +// GetByBookshelfID retrieves books belonging to a specific bookshelf ID. +func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { + objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) + if err != nil { + return nil, fmt.Errorf("invalid bookshelf ID: %w", err) + } + + matchStage := bson.D{{"$match", bson.D{{"bookshelf_id", objectBookshelfID}}}} + skipStage := bson.D{{"$skip", (page - 1) * limit}} + limitStage := bson.D{{"$limit", limit}} + + cursor, err := r.collection.Aggregate(ctx, mongo.Pipeline{matchStage, skipStage, limitStage}) + if err != nil { + r.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get books by bookshelf id: %w", err) + } + defer cursor.Close(ctx) + + var books []*models.Book + if err = cursor.All(ctx, &books); err != nil { + return nil, fmt.Errorf("failed to decode books: %w", err) + } + + return books, nil +} + +// Update updates a book in the database. +func (r *BookRepo) Update(ctx context.Context, id string, update *models.BookUpdate) error { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return fmt.Errorf("invalid book ID: %w", err) + } + + // Convert BookUpdate to bson.M + updateBson := bson.M{} + if update.Title != nil { + updateBson["title"] = *update.Title + } + if update.Author != nil { + updateBson["author"] = *update.Author + } + // ... similarly for other fields ... + + updateBson["updated_at"] = time.Now() + _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": updateBson}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookNotFound + } + return fmt.Errorf("failed to update book: %w", err) + } + return nil +} + +// Delete removes a book from the database. +func (r *BookRepo) Delete(ctx context.Context, id string) error { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return fmt.Errorf("invalid book ID: %w", err) + } + + _, err = r.collection.DeleteOne(ctx, bson.M{"_id": objectID}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookNotFound + } + return fmt.Errorf("failed to delete book: %w", err) + } + return nil +} From b974aa89e1fb78ae6e285bf5724743297c6f5e33 Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 03:02:49 +0300 Subject: [PATCH 31/56] implement Mongo BookshelfRepo and UserRepo --- internal/server/storage/mongo/bookshelf.go | 142 +++++++++++++++++++++ internal/server/storage/mongo/user.go | 97 ++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 internal/server/storage/mongo/bookshelf.go create mode 100644 internal/server/storage/mongo/user.go diff --git a/internal/server/storage/mongo/bookshelf.go b/internal/server/storage/mongo/bookshelf.go new file mode 100644 index 0000000..886007d --- /dev/null +++ b/internal/server/storage/mongo/bookshelf.go @@ -0,0 +1,142 @@ +package mongo + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log/slog" + "time" +) + +// ErrBookshelfNotFound occurs when a bookshelf is not found in the database. +var ErrBookshelfNotFound = errors.New("bookshelf not found") + +// ErrBookshelfAlreadyExists occurs when trying to create a bookshelf with an ID that already exists. +var ErrBookshelfAlreadyExists = errors.New("bookshelf already exists") + +// BookshelfRepo implements the repository.BookshelfRepo interface for MongoDB. +type BookshelfRepo struct { + collection *mongo.Collection + log *slog.Logger +} + +// NewBookshelfRepo creates a new BookshelfRepo instance. +func NewBookshelfRepo(db *mongo.Database, log *slog.Logger) repository.BookshelfRepo { + return &BookshelfRepo{ + collection: db.Collection("bookshelves"), + log: log, + } +} + +// Create inserts a new bookshelf into the database. +func (r *BookshelfRepo) Create(ctx context.Context, bookshelf *models.Bookshelf) error { + bookshelf.ID = primitive.NewObjectID().Hex() + bookshelf.CreatedAt = time.Now() + bookshelf.UpdatedAt = time.Now() + + _, err := r.collection.InsertOne(ctx, bookshelf) + if err != nil { + // Check for duplicate key error + var writeErr mongo.WriteException + if errors.As(err, &writeErr) && writeErr.WriteErrors[0].Code == 11000 { + return ErrBookshelfAlreadyExists + } + + r.log.Error("failed to create bookshelf", slog.Any("error", err)) + return fmt.Errorf("failed to create bookshelf: %w", err) + } + + return nil +} + +// GetByID retrieves a bookshelf from the database by its ID. +func (r *BookshelfRepo) GetByID(ctx context.Context, id string) (*models.Bookshelf, error) { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, fmt.Errorf("invalid bookshelf ID: %w", err) + } + + var bookshelf models.Bookshelf + err = r.collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&bookshelf) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrBookshelfNotFound + } + return nil, fmt.Errorf("failed to get bookshelf: %w", err) + } + + return &bookshelf, nil +} + +// GetByUserID retrieves bookshelves associated with a specific user ID. +func (r *BookshelfRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { + objectUserID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return nil, fmt.Errorf("invalid user ID: %w", err) + } + + findOptions := options.Find() + findOptions.SetSkip((page - 1) * limit) + findOptions.SetLimit(limit) + + cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) + if err != nil { + r.log.Error("failed to get bookshelves by user ID", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get bookshelves by user ID: %w", err) + } + defer cursor.Close(ctx) + + var bookshelves []*models.Bookshelf + if err = cursor.All(ctx, &bookshelves); err != nil { + return nil, fmt.Errorf("failed to decode bookshelves: %w", err) + } + + return bookshelves, nil +} + +// Update updates a bookshelf in the database. +func (r *BookshelfRepo) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return fmt.Errorf("invalid bookshelf ID: %w", err) + } + + // Convert BookshelfUpdate to bson.M + updateBson := bson.M{} + if update.Name != nil { + updateBson["name"] = *update.Name + } + + updateBson["updated_at"] = time.Now() + _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": updateBson}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to update bookshelf: %w", err) + } + return nil +} + +// Delete removes a bookshelf from the database. +func (r *BookshelfRepo) Delete(ctx context.Context, id string) error { + objectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + return fmt.Errorf("invalid bookshelf ID: %w", err) + } + + _, err = r.collection.DeleteOne(ctx, bson.M{"_id": objectID}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to delete bookshelf: %w", err) + } + return nil +} diff --git a/internal/server/storage/mongo/user.go b/internal/server/storage/mongo/user.go new file mode 100644 index 0000000..2d59b99 --- /dev/null +++ b/internal/server/storage/mongo/user.go @@ -0,0 +1,97 @@ +package mongo + +import ( + "context" + "errors" + "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "log/slog" + "time" +) + +// ErrUserNotFound occurs when a user is not found in the database. +var ErrUserNotFound = errors.New("user not found") + +// ErrUserAlreadyExists occurs when trying to create a user with an ID that already exists. +var ErrUserAlreadyExists = errors.New("user already exists") + +// UserRepo implements the repository.UserRepo interface for MongoDB. +type UserRepo struct { + collection *mongo.Collection + log *slog.Logger +} + +// NewUserRepo creates a new UserRepo instance. +func NewUserRepo(db *mongo.Database, log *slog.Logger) repository.UserRepo { + return &UserRepo{ + collection: db.Collection("users"), + log: log, + } +} + +// Create inserts a new user into the database. +func (r *UserRepo) Create(ctx context.Context, user *models.User) error { + user.CreatedAt = time.Now() + user.UpdatedAt = time.Now() + + _, err := r.collection.InsertOne(ctx, user) + if err != nil { + // Check for duplicate key error + var writeErr mongo.WriteException + if errors.As(err, &writeErr) && writeErr.WriteErrors[0].Code == 11000 { + return ErrUserAlreadyExists + } + + r.log.Error("failed to create user", slog.Any("error", err)) + return fmt.Errorf("failed to create user: %w", err) + } + + return nil +} + +// GetByID retrieves a user from the database by their ID. +func (r *UserRepo) GetByID(ctx context.Context, id string) (*models.User, error) { + var user models.User + err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&user) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + return &user, nil +} + +// Update updates a user in the database. +func (r *UserRepo) Update(ctx context.Context, id string, update *models.UserUpdate) error { + // Convert UserUpdate to bson.M + updateBson := bson.M{} + if update.DisplayName != nil { + updateBson["display_name"] = *update.DisplayName + } + updateBson["updated_at"] = time.Now() + + _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": updateBson}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrUserNotFound + } + return fmt.Errorf("failed to update user: %w", err) + } + return nil +} + +// Delete removes a user from the database. +func (r *UserRepo) Delete(ctx context.Context, id string) error { + _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return ErrUserNotFound + } + return fmt.Errorf("failed to delete user: %w", err) + } + return nil +} From f5296d1b03ee5e49d8715b222edf19092aa97230 Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 06:59:21 +0300 Subject: [PATCH 32/56] remove redundant code --- internal/server/models/book.go | 27 ++++++---------------- internal/server/models/bookshelf.go | 14 ++--------- internal/server/models/user.go | 12 ++-------- internal/server/storage/mongo/book.go | 14 ++--------- internal/server/storage/mongo/bookshelf.go | 10 ++------ internal/server/storage/mongo/user.go | 10 ++------ 6 files changed, 17 insertions(+), 70 deletions(-) diff --git a/internal/server/models/book.go b/internal/server/models/book.go index 19240f3..6a4327b 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -1,13 +1,13 @@ package models import ( - "go.mongodb.org/mongo-driver/bson" "time" ) // Book represents a book in the library. type Book struct { ID string `bson:"_id,omitempty" json:"id"` + UserID string `bson:"user_id" json:"user_id"` BookshelfID string `bson:"bookshelf_id" json:"bookshelf_id"` Title string `bson:"title" json:"title"` Author string `bson:"author" json:"author"` @@ -18,25 +18,12 @@ type Book struct { UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } -// ToMap converts the Book struct to a bson.M map for MongoDB updates. -func (b *Book) ToMap() bson.M { - return bson.M{ - "bookshelf_id": b.BookshelfID, - "title": b.Title, - "author": b.Author, - "isbn": b.ISBN, - "description": b.Description, - "cover_image": b.CoverImage, - "created_at": b.CreatedAt, - "updated_at": b.UpdatedAt, - } -} - // BookUpdate represents fields that can be updated in a Book. type BookUpdate struct { - Title *string `bson:"title,omitempty" json:"title,omitempty"` // Optional fields for update - Author *string `bson:"author,omitempty" json:"author,omitempty"` - ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` - Description *string `bson:"description,omitempty" json:"description,omitempty"` - CoverImage *string `bson:"cover_image,omitempty" json:"cover_image,omitempty"` + Title *string `bson:"title,omitempty" json:"title,omitempty"` // Optional fields for update + Author *string `bson:"author,omitempty" json:"author,omitempty"` + ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` + Description *string `bson:"description,omitempty" json:"description,omitempty"` + CoverImage *string `bson:"cover_image,omitempty" json:"cover_image,omitempty"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } diff --git a/internal/server/models/bookshelf.go b/internal/server/models/bookshelf.go index a30da21..29657bd 100644 --- a/internal/server/models/bookshelf.go +++ b/internal/server/models/bookshelf.go @@ -1,7 +1,6 @@ package models import ( - "go.mongodb.org/mongo-driver/bson" "time" ) @@ -14,17 +13,8 @@ type Bookshelf struct { UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } -// ToMap converts the Bookshelf struct to a bson.M map for MongoDB updates. -func (b *Bookshelf) ToMap() bson.M { - return bson.M{ - "user_id": b.UserID, - "name": b.Name, - "created_at": b.CreatedAt, - "updated_at": b.UpdatedAt, - } -} - // BookshelfUpdate represents fields that can be updated in a Bookshelf. type BookshelfUpdate struct { - Name *string `bson:"name,omitempty" json:"name,omitempty"` // Optional field for update + Name *string `bson:"name,omitempty" json:"name,omitempty"` // Optional field for update + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } diff --git a/internal/server/models/user.go b/internal/server/models/user.go index f782caa..4a98cad 100644 --- a/internal/server/models/user.go +++ b/internal/server/models/user.go @@ -1,7 +1,6 @@ package models import ( - "go.mongodb.org/mongo-driver/bson" "time" ) @@ -13,15 +12,8 @@ type User struct { UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } -func (u *User) ToMap() bson.M { - return bson.M{ - "display_name": u.DisplayName, - "created_at": u.CreatedAt, - "updated_at": u.UpdatedAt, - } -} - // UserUpdate represents fields that can be updated in a User. type UserUpdate struct { - DisplayName *string `bson:"display_name,omitempty" json:"display_name,omitempty"` // Optional field for update + DisplayName *string `bson:"display_name,omitempty" json:"display_name,omitempty"` // Optional field for update + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index b78ae89..f665496 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -131,18 +131,8 @@ func (r *BookRepo) Update(ctx context.Context, id string, update *models.BookUpd return fmt.Errorf("invalid book ID: %w", err) } - // Convert BookUpdate to bson.M - updateBson := bson.M{} - if update.Title != nil { - updateBson["title"] = *update.Title - } - if update.Author != nil { - updateBson["author"] = *update.Author - } - // ... similarly for other fields ... - - updateBson["updated_at"] = time.Now() - _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": updateBson}) + update.UpdatedAt = time.Now() + _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookNotFound diff --git a/internal/server/storage/mongo/bookshelf.go b/internal/server/storage/mongo/bookshelf.go index 886007d..31db188 100644 --- a/internal/server/storage/mongo/bookshelf.go +++ b/internal/server/storage/mongo/bookshelf.go @@ -107,14 +107,8 @@ func (r *BookshelfRepo) Update(ctx context.Context, id string, update *models.Bo return fmt.Errorf("invalid bookshelf ID: %w", err) } - // Convert BookshelfUpdate to bson.M - updateBson := bson.M{} - if update.Name != nil { - updateBson["name"] = *update.Name - } - - updateBson["updated_at"] = time.Now() - _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": updateBson}) + update.UpdatedAt = time.Now() + _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookshelfNotFound diff --git a/internal/server/storage/mongo/user.go b/internal/server/storage/mongo/user.go index 2d59b99..0cc2c12 100644 --- a/internal/server/storage/mongo/user.go +++ b/internal/server/storage/mongo/user.go @@ -67,14 +67,8 @@ func (r *UserRepo) GetByID(ctx context.Context, id string) (*models.User, error) // Update updates a user in the database. func (r *UserRepo) Update(ctx context.Context, id string, update *models.UserUpdate) error { - // Convert UserUpdate to bson.M - updateBson := bson.M{} - if update.DisplayName != nil { - updateBson["display_name"] = *update.DisplayName - } - updateBson["updated_at"] = time.Now() - - _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": updateBson}) + update.UpdatedAt = time.Now() + _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrUserNotFound From 46744d09d025542084835856fa53fb381c155bb2 Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 21:02:35 +0300 Subject: [PATCH 33/56] improve: add Business logic to BookService --- internal/server/middlewares/auth.go | 7 +- internal/server/services/books/books.go | 170 ++++++++++++------------ 2 files changed, 93 insertions(+), 84 deletions(-) diff --git a/internal/server/middlewares/auth.go b/internal/server/middlewares/auth.go index 275b36c..7cc249a 100644 --- a/internal/server/middlewares/auth.go +++ b/internal/server/middlewares/auth.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" ) +// AuthMiddleware is a Gin middleware for authenticating requests using Firebase func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") @@ -34,8 +35,10 @@ func AuthMiddleware() gin.HandlerFunc { return } - // Set the UID in the context for further use - c.Set("uid", token.UID) + // Add the UID to the context for further use. + ctx := context.WithValue(c.Request.Context(), "userID", token.UID) + c.Request = c.Request.WithContext(ctx) // Update the request's context + c.Next() } } diff --git a/internal/server/services/books/books.go b/internal/server/services/books/books.go index 4e707fe..5ed0f72 100644 --- a/internal/server/services/books/books.go +++ b/internal/server/services/books/books.go @@ -2,131 +2,137 @@ package books import ( "context" - "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "github.com/getz-devs/librakeeper-server/internal/server/repository" "log/slog" - "time" -) - -var ( - ErrBookNotFound = errors.New("book not found") ) +// BookService defines the interface for book service operations. type BookService struct { - collection *mongo.Collection - log *slog.Logger + repo repository.BookRepo + bookshelfRepo repository.BookshelfRepo + log *slog.Logger + bookLimit int // Limit for books per bookshelf } -func NewBookService(collection *mongo.Collection, log *slog.Logger) *BookService { +// NewBookService creates a new BookService instance. +func NewBookService(repo repository.BookRepo, bookshelfRepo repository.BookshelfRepo, log *slog.Logger) *BookService { return &BookService{ - collection: collection, - log: log, + repo: repo, + bookshelfRepo: bookshelfRepo, + log: log, + bookLimit: 1000, // Hardcoded for now, TODO: read from config } } -func (s *BookService) CreateBook(ctx context.Context, book *models.Book) (*models.Book, error) { - book.CreatedAt = time.Now() - book.UpdatedAt = time.Now() +// Create creates a new book. +func (s *BookService) Create(ctx context.Context, book *models.Book) error { + // Rule 2: Book Title & Author Presence + if book.Title == "" || book.Author == "" { + return fmt.Errorf("book title and author are required") + } - res, err := s.collection.InsertOne(ctx, book) - if err != nil { - s.log.Error("failed to create book", slog.Any("error", err)) - return nil, fmt.Errorf("failed to create book: %w", err) + // Rule 3: Bookshelf Ownership + userID, ok := ctx.Value("userID").(string) + if !ok { + return fmt.Errorf("userID not found in context") } - book.ID = res.InsertedID.(primitive.ObjectID) - return book, nil -} + bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) + if err != nil { + return fmt.Errorf("failed to get bookshelf: %w", err) + } + if bookshelf.UserID != userID { + return fmt.Errorf("user is not authorized to add a book to this bookshelf") + } -func (s *BookService) GetBook(ctx context.Context, bookID primitive.ObjectID) (*models.Book, error) { - var book models.Book - err := s.collection.FindOne(ctx, bson.M{"_id": bookID}).Decode(&book) + // TODO: optimize + // Rule 4: Book Limit per Bookshelf + books, err := s.repo.GetByBookshelfID(ctx, book.BookshelfID, 1, int64(s.bookLimit)) // Get up to the limit if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return nil, ErrBookNotFound + return fmt.Errorf("failed to get books for bookshelf: %w", err) + } + if len(books) >= s.bookLimit { + return fmt.Errorf("bookshelf has reached the book limit (%d)", s.bookLimit) + } + + // Rule 5: Unique Book within Bookshelf + for _, existingBook := range books { + if existingBook.ISBN == book.ISBN { + return fmt.Errorf("book with ISBN '%s' already exists in this bookshelf", book.ISBN) } - return nil, fmt.Errorf("failed to get book: %w", err) } - return &book, nil -} -func (s *BookService) GetBooks(ctx context.Context, page int64, limit int64) ([]*models.Book, error) { - findOptions := options.Find() - findOptions.SetSkip((page - 1) * limit) - findOptions.SetLimit(limit) + if err := s.repo.Create(ctx, book); err != nil { + return fmt.Errorf("failed to create book: %w", err) + } + return nil +} - cursor, err := s.collection.Find(ctx, bson.M{}, findOptions) +// GetByID retrieves a book by its ID. +func (s *BookService) GetByID(ctx context.Context, bookID string) (*models.Book, error) { + book, err := s.repo.GetByID(ctx, bookID) if err != nil { - s.log.Error("failed to get books", slog.Any("error", err)) - return nil, fmt.Errorf("failed to get books: %w", err) + return nil, fmt.Errorf("failed to get book: %w", err) } - defer cursor.Close(ctx) + return book, nil +} - var books []*models.Book - for cursor.Next(ctx) { - var book models.Book - if err := cursor.Decode(&book); err != nil { - return nil, fmt.Errorf("failed to decode book: %w", err) - } - books = append(books, &book) +// GetByUserID retrieves a list of books for a specific user. +func (s *BookService) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { + books, err := s.repo.GetByUserID(ctx, userID, page, limit) + if err != nil { + return nil, fmt.Errorf("failed to get books by user ID: %w", err) } + return books, nil +} - if err := cursor.Err(); err != nil { - return nil, fmt.Errorf("cursor error: %w", err) +// GetByBookshelfID retrieves books by bookshelf ID. +func (s *BookService) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { + books, err := s.repo.GetByBookshelfID(ctx, bookshelfID, page, limit) + if err != nil { + return nil, fmt.Errorf("failed to get books by bookshelf ID: %w", err) } return books, nil } -func (s *BookService) GetBooksByBookshelfID(ctx context.Context, bookshelfID primitive.ObjectID, page int64, limit int64) ([]*models.Book, error) { - matchStage := bson.D{{"$match", bson.D{{"bookshelf_id", bookshelfID}}}} - skipStage := bson.D{{"$skip", (page - 1) * limit}} - limitStage := bson.D{{"$limit", limit}} +// Update updates an existing book. +func (s *BookService) Update(ctx context.Context, bookID string, update *models.BookUpdate) error { + // 1. Get the book + book, err := s.repo.GetByID(ctx, bookID) + if err != nil { + return fmt.Errorf("failed to get book: %w", err) + } - cursor, err := s.collection.Aggregate(ctx, mongo.Pipeline{matchStage, skipStage, limitStage}) + // 2. Get the bookshelf + bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) if err != nil { - s.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) - return nil, fmt.Errorf("failed to get books by bookshelf id: %w", err) + return fmt.Errorf("failed to get bookshelf: %w", err) } - defer cursor.Close(ctx) - var books []*models.Book - for cursor.Next(ctx) { - var book models.Book - if err := cursor.Decode(&book); err != nil { - return nil, fmt.Errorf("failed to decode book: %w", err) - } - books = append(books, &book) + // 3. Get userID from context + userID, ok := ctx.Value("userID").(string) + if !ok { + return fmt.Errorf("userID not found in context") } - if err := cursor.Err(); err != nil { - return nil, fmt.Errorf("cursor error: %w", err) + // 4. Check bookshelf ownership + if bookshelf.UserID != userID { + return fmt.Errorf("user is not authorized to modify this book") } - return books, nil -} -func (s *BookService) UpdateBook(ctx context.Context, bookID primitive.ObjectID, update bson.M) error { - update["updated_at"] = time.Now() - _, err := s.collection.UpdateOne(ctx, bson.M{"_id": bookID}, bson.M{"$set": update}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return ErrBookNotFound - } + // 5. If authorized, proceed with the update: + if err := s.repo.Update(ctx, bookID, update); err != nil { return fmt.Errorf("failed to update book: %w", err) } + return nil } -func (s *BookService) DeleteBook(ctx context.Context, bookID primitive.ObjectID) error { - _, err := s.collection.DeleteOne(ctx, bson.M{"_id": bookID}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return ErrBookNotFound - } +// Delete deletes a book. +func (s *BookService) Delete(ctx context.Context, bookID string) error { + if err := s.repo.Delete(ctx, bookID); err != nil { return fmt.Errorf("failed to delete book: %w", err) } return nil From db1e2d4e5ef388328e4d0c371e5ebcdbe97b5360 Mon Sep 17 00:00:00 2001 From: Den Date: Fri, 5 Jul 2024 21:54:19 +0300 Subject: [PATCH 34/56] Book model finished: repo, service and handler are ready --- internal/server/handlers/books.go | 98 ++++++++++++++----------- internal/server/repository/book.go | 2 + internal/server/services/books/books.go | 91 +++++++++++++++++++---- internal/server/storage/mongo/book.go | 32 ++++++++ 4 files changed, 165 insertions(+), 58 deletions(-) diff --git a/internal/server/handlers/books.go b/internal/server/handlers/books.go index 106b1a5..427fdb3 100644 --- a/internal/server/handlers/books.go +++ b/internal/server/handlers/books.go @@ -1,11 +1,12 @@ package handlers import ( + "context" "errors" + "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/services/books" "github.com/gin-gonic/gin" - "go.mongodb.org/mongo-driver/bson/primitive" "log/slog" "net/http" "strconv" @@ -30,28 +31,28 @@ func (h *BookHandlers) CreateBook(c *gin.Context) { return } - ctx := c.Request.Context() - createdBook, err := h.service.CreateBook(ctx, &book) - if err != nil { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + if err := h.service.Create(ctx, &book); err != nil { h.log.Error("failed to create book", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) return } - c.JSON(http.StatusCreated, createdBook) + c.JSON(http.StatusCreated, book) } func (h *BookHandlers) GetBook(c *gin.Context) { - bookIDHex := c.Param("id") - - bookID, err := primitive.ObjectIDFromHex(bookIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) - return - } + bookID := c.Param("id") ctx := c.Request.Context() - book, err := h.service.GetBook(ctx, bookID) + book, err := h.service.GetByID(ctx, bookID) if err != nil { if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) @@ -65,7 +66,7 @@ func (h *BookHandlers) GetBook(c *gin.Context) { c.JSON(http.StatusOK, book) } -func (h *BookHandlers) GetBooks(c *gin.Context) { +func (h *BookHandlers) GetBooksByUserID(c *gin.Context) { pageStr := c.DefaultQuery("page", "1") limitStr := c.DefaultQuery("limit", "10") @@ -81,8 +82,14 @@ func (h *BookHandlers) GetBooks(c *gin.Context) { return } + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + ctx := c.Request.Context() - result, err := h.service.GetBooks(ctx, page, limit) + result, err := h.service.GetByUserID(ctx, userID.(string), page, limit) if err != nil { h.log.Error("failed to get result", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result"}) @@ -93,13 +100,7 @@ func (h *BookHandlers) GetBooks(c *gin.Context) { } func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { - bookshelfIDHex := c.Param("bookshelfId") - - bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) - return - } + bookshelfID := c.Param("bookshelfId") pageStr := c.DefaultQuery("page", "1") limitStr := c.DefaultQuery("limit", "10") @@ -116,8 +117,15 @@ func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { return } - ctx := c.Request.Context() - result, err := h.service.GetBooksByBookshelfID(ctx, bookshelfID, page, limit) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + result, err := h.service.GetByBookshelfID(ctx, bookshelfID, page, limit) if err != nil { h.log.Error("failed to get result by bookshelf id", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result by bookshelf ID"}) @@ -128,28 +136,34 @@ func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { } func (h *BookHandlers) UpdateBook(c *gin.Context) { - bookIDHex := c.Param("id") + bookID := c.Param("id") - bookID, err := primitive.ObjectIDFromHex(bookIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) + var update models.BookUpdate + if err := c.BindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - var update models.Book - if err := c.BindJSON(&update); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - ctx := c.Request.Context() - err = h.service.UpdateBook(ctx, bookID, update.ToMap()) - if err != nil { + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + if err := h.service.Update(ctx, bookID, &update); err != nil { if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } - h.log.Error("failed to update book", slog.Any("error", err)) + + h.log.Error( + "failed to update book", + slog.Any("error", err), + slog.String("bookID", bookID), + slog.String("userID", fmt.Sprintf("%v", userID)), + ) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update book"}) return } @@ -158,17 +172,17 @@ func (h *BookHandlers) UpdateBook(c *gin.Context) { } func (h *BookHandlers) DeleteBook(c *gin.Context) { - bookIDHex := c.Param("id") + bookID := c.Param("id") - bookID, err := primitive.ObjectIDFromHex(bookIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid book ID"}) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - ctx := c.Request.Context() - err = h.service.DeleteBook(ctx, bookID) - if err != nil { + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + if err := h.service.Delete(ctx, bookID); err != nil { if errors.Is(err, books.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return diff --git a/internal/server/repository/book.go b/internal/server/repository/book.go index 8840e56..57c28c2 100644 --- a/internal/server/repository/book.go +++ b/internal/server/repository/book.go @@ -11,6 +11,8 @@ type BookRepo interface { GetByID(ctx context.Context, id string) (*models.Book, error) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) + CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) + ExistsInBookshelf(ctx context.Context, isbn, bookshelfID string) (bool, error) Update(ctx context.Context, id string, update *models.BookUpdate) error Delete(ctx context.Context, id string) error } diff --git a/internal/server/services/books/books.go b/internal/server/services/books/books.go index 5ed0f72..fcee459 100644 --- a/internal/server/services/books/books.go +++ b/internal/server/services/books/books.go @@ -2,18 +2,31 @@ package books import ( "context" + "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/repository" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" ) +// Custom Error Types: +var ( + ErrBookNotFound = errors.New("book not found") + ErrBookshelfNotFound = errors.New("bookshelf not found") + ErrUserNotFoundInContext = errors.New("userID not found in context") + ErrNotAuthorized = errors.New("user is not authorized to perform this action") + ErrTitleAndAuthorRequired = errors.New("book title and author are required") + ErrBookshelfLimitReached = errors.New("bookshelf has reached the book limit") + ErrBookAlreadyExists = errors.New("book with this ISBN already exists in this bookshelf") +) + // BookService defines the interface for book service operations. type BookService struct { repo repository.BookRepo bookshelfRepo repository.BookshelfRepo log *slog.Logger - bookLimit int // Limit for books per bookshelf + bookLimit int } // NewBookService creates a new BookService instance. @@ -22,7 +35,7 @@ func NewBookService(repo repository.BookRepo, bookshelfRepo repository.Bookshelf repo: repo, bookshelfRepo: bookshelfRepo, log: log, - bookLimit: 1000, // Hardcoded for now, TODO: read from config + bookLimit: 1000, // TODO: Read from config } } @@ -30,43 +43,49 @@ func NewBookService(repo repository.BookRepo, bookshelfRepo repository.Bookshelf func (s *BookService) Create(ctx context.Context, book *models.Book) error { // Rule 2: Book Title & Author Presence if book.Title == "" || book.Author == "" { - return fmt.Errorf("book title and author are required") + return ErrTitleAndAuthorRequired } // Rule 3: Bookshelf Ownership userID, ok := ctx.Value("userID").(string) if !ok { - return fmt.Errorf("userID not found in context") + return ErrUserNotFoundInContext } bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) if err != nil { + if errors.Is(err, mongo.ErrBookshelfNotFound) { + return ErrBookshelfNotFound + } return fmt.Errorf("failed to get bookshelf: %w", err) } + if bookshelf.UserID != userID { - return fmt.Errorf("user is not authorized to add a book to this bookshelf") + return ErrNotAuthorized } - // TODO: optimize // Rule 4: Book Limit per Bookshelf - books, err := s.repo.GetByBookshelfID(ctx, book.BookshelfID, 1, int64(s.bookLimit)) // Get up to the limit + bookCount, err := s.repo.CountInBookshelf(ctx, book.BookshelfID) if err != nil { - return fmt.Errorf("failed to get books for bookshelf: %w", err) + return fmt.Errorf("failed to get book count for bookshelf: %w", err) } - if len(books) >= s.bookLimit { - return fmt.Errorf("bookshelf has reached the book limit (%d)", s.bookLimit) + if bookCount >= s.bookLimit { + return ErrBookshelfLimitReached } // Rule 5: Unique Book within Bookshelf - for _, existingBook := range books { - if existingBook.ISBN == book.ISBN { - return fmt.Errorf("book with ISBN '%s' already exists in this bookshelf", book.ISBN) - } + exists, err := s.repo.ExistsInBookshelf(ctx, book.ISBN, book.BookshelfID) + if err != nil { + return fmt.Errorf("failed to check book existence: %w", err) + } + if exists { + return ErrBookAlreadyExists } if err := s.repo.Create(ctx, book); err != nil { return fmt.Errorf("failed to create book: %w", err) } + return nil } @@ -74,6 +93,9 @@ func (s *BookService) Create(ctx context.Context, book *models.Book) error { func (s *BookService) GetByID(ctx context.Context, bookID string) (*models.Book, error) { book, err := s.repo.GetByID(ctx, bookID) if err != nil { + if errors.Is(err, mongo.ErrBookNotFound) { + return nil, ErrBookNotFound + } return nil, fmt.Errorf("failed to get book: %w", err) } return book, nil @@ -102,24 +124,30 @@ func (s *BookService) Update(ctx context.Context, bookID string, update *models. // 1. Get the book book, err := s.repo.GetByID(ctx, bookID) if err != nil { + if errors.Is(err, mongo.ErrBookNotFound) { + return ErrBookNotFound + } return fmt.Errorf("failed to get book: %w", err) } // 2. Get the bookshelf bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) if err != nil { + if errors.Is(err, mongo.ErrBookshelfNotFound) { + return ErrBookshelfNotFound + } return fmt.Errorf("failed to get bookshelf: %w", err) } // 3. Get userID from context userID, ok := ctx.Value("userID").(string) if !ok { - return fmt.Errorf("userID not found in context") + return ErrUserNotFoundInContext } // 4. Check bookshelf ownership if bookshelf.UserID != userID { - return fmt.Errorf("user is not authorized to modify this book") + return ErrNotAuthorized } // 5. If authorized, proceed with the update: @@ -132,8 +160,39 @@ func (s *BookService) Update(ctx context.Context, bookID string, update *models. // Delete deletes a book. func (s *BookService) Delete(ctx context.Context, bookID string) error { + // 1. Get the book + book, err := s.repo.GetByID(ctx, bookID) + if err != nil { + if errors.Is(err, mongo.ErrBookNotFound) { + return ErrBookNotFound + } + return fmt.Errorf("failed to get book: %w", err) + } + + // 2. Get the bookshelf + bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) + if err != nil { + if errors.Is(err, mongo.ErrBookshelfNotFound) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to get bookshelf: %w", err) + } + + // 3. Get userID from context + userID, ok := ctx.Value("userID").(string) + if !ok { + return ErrUserNotFoundInContext + } + + // 4. Check bookshelf ownership + if bookshelf.UserID != userID { + return ErrNotAuthorized + } + + // 5. If authorized, proceed to delete: if err := s.repo.Delete(ctx, bookID); err != nil { return fmt.Errorf("failed to delete book: %w", err) } + return nil } diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index f665496..09acf1b 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -124,6 +124,38 @@ func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, pag return books, nil } +// CountInBookshelf returns the number of books in a bookshelf. +func (r *BookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) { + objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) + if err != nil { + return 0, fmt.Errorf("invalid bookshelf ID: %w", err) + } + + count, err := r.collection.CountDocuments(ctx, bson.M{"bookshelf_id": objectBookshelfID}) + if err != nil { + r.log.Error("failed to count books by bookshelf ID", slog.Any("error", err)) + return 0, fmt.Errorf("failed to count books by bookshelf ID: %w", err) + } + + return int(count), nil +} + +// ExistsInBookshelf checks if a book with the given ISBN already exists in the bookshelf. +func (r *BookRepo) ExistsInBookshelf(ctx context.Context, isbn, bookshelfID string) (bool, error) { + objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) + if err != nil { + return false, fmt.Errorf("invalid bookshelf ID: %w", err) + } + + count, err := r.collection.CountDocuments(ctx, bson.M{"isbn": isbn, "bookshelf_id": objectBookshelfID}) + if err != nil { + r.log.Error("failed to check book existence", slog.Any("error", err)) + return false, fmt.Errorf("failed to check book existence: %w", err) + } + + return count > 0, nil +} + // Update updates a book in the database. func (r *BookRepo) Update(ctx context.Context, id string, update *models.BookUpdate) error { objectID, err := primitive.ObjectIDFromHex(id) From 474ccaf7ea7494a00f3a000d8289c63c8e8ffe1d Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 6 Jul 2024 10:23:48 +0300 Subject: [PATCH 35/56] Bookshelf model finished: repo, service and handler are ready --- internal/server/handlers/bookshelves.go | 102 ++++++------ internal/server/repository/bookshelf.go | 4 +- .../services/bookshelves/bookshelves.go | 145 +++++++++++------- internal/server/storage/mongo/bookshelf.go | 36 ++++- 4 files changed, 182 insertions(+), 105 deletions(-) diff --git a/internal/server/handlers/bookshelves.go b/internal/server/handlers/bookshelves.go index 2c5718e..11438b1 100644 --- a/internal/server/handlers/bookshelves.go +++ b/internal/server/handlers/bookshelves.go @@ -1,22 +1,24 @@ package handlers import ( + "context" "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelves" "github.com/gin-gonic/gin" - "go.mongodb.org/mongo-driver/bson/primitive" "log/slog" "net/http" "strconv" ) +// BookshelfHandlers handles HTTP requests related to bookshelves. type BookshelfHandlers struct { service *bookshelves.BookshelfService log *slog.Logger } +// NewBookshelfHandlers creates a new BookshelfHandlers instance. func NewBookshelfHandlers(service *bookshelves.BookshelfService, log *slog.Logger) *BookshelfHandlers { return &BookshelfHandlers{ service: service, @@ -24,8 +26,9 @@ func NewBookshelfHandlers(service *bookshelves.BookshelfService, log *slog.Logge } } -func (h *BookshelfHandlers) CreateBookshelf(c *gin.Context) { - userID, exists := c.Get("uid") +// Create creates a new bookshelf. +func (h *BookshelfHandlers) Create(c *gin.Context) { + userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return @@ -37,29 +40,27 @@ func (h *BookshelfHandlers) CreateBookshelf(c *gin.Context) { return } - bookshelf.UserID = userID.(string) // Type assertion to string - ctx := c.Request.Context() - createdBookshelf, err := h.service.CreateBookshelf(ctx, &bookshelf) - if err != nil { + ctx := context.WithValue(c.Request.Context(), "userID", userID) + if err := h.service.Create(ctx, &bookshelf); err != nil { + if errors.Is(err, bookshelves.ErrNameRequired) || errors.Is(err, bookshelves.ErrBookshelfAlreadyExists) { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to create bookshelf", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bookshelf"}) return } - c.JSON(http.StatusCreated, createdBookshelf) + c.JSON(http.StatusCreated, bookshelf) } -func (h *BookshelfHandlers) GetBookshelf(c *gin.Context) { - bookshelfIDHex := c.Param("id") - - bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) - return - } +// GetByID retrieves a bookshelf by ID. +func (h *BookshelfHandlers) GetByID(c *gin.Context) { + bookshelfID := c.Param("id") ctx := c.Request.Context() - bookshelf, err := h.service.GetBookshelf(ctx, bookshelfID) + bookshelf, err := h.service.GetByID(ctx, bookshelfID) if err != nil { if errors.Is(err, bookshelves.ErrBookshelfNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) @@ -73,19 +74,14 @@ func (h *BookshelfHandlers) GetBookshelf(c *gin.Context) { c.JSON(http.StatusOK, bookshelf) } -func (h *BookshelfHandlers) GetBookshelvesByUserID(c *gin.Context) { - userIDHex, exists := c.Get("uid") +// GetByUser retrieves bookshelves for a user. +func (h *BookshelfHandlers) GetByUser(c *gin.Context) { + userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - userID, err := primitive.ObjectIDFromHex(fmt.Sprintf("%v", userIDHex)) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error parsing user id"}) - return - } - pageStr := c.DefaultQuery("page", "1") limitStr := c.DefaultQuery("limit", "10") @@ -102,39 +98,46 @@ func (h *BookshelfHandlers) GetBookshelvesByUserID(c *gin.Context) { } ctx := c.Request.Context() - result, err := h.service.GetBookshelvesByUserID(ctx, userID, page, limit) + result, err := h.service.GetByUser(ctx, userID.(string), page, limit) if err != nil { h.log.Error("failed to get result by user id", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result by user ID"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelves by user ID"}) return } c.JSON(http.StatusOK, result) } -func (h *BookshelfHandlers) UpdateBookshelf(c *gin.Context) { - bookshelfIDHex := c.Param("id") +// Update updates a bookshelf. +func (h *BookshelfHandlers) Update(c *gin.Context) { + bookshelfID := c.Param("id") - bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + var update models.BookshelfUpdate + if err := c.BindJSON(&update); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - var update models.Bookshelf - if err := c.BindJSON(&update); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - ctx := c.Request.Context() - err = h.service.UpdateBookshelf(ctx, bookshelfID, update.ToMap()) - if err != nil { - if errors.Is(err, bookshelves.ErrBookshelfNotFound) { + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + if err := h.service.Update(ctx, bookshelfID, &update); err != nil { + if errors.Is(err, bookshelves.ErrBookshelfNotFound) || errors.Is(err, bookshelves.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } - h.log.Error("failed to update bookshelf", slog.Any("error", err)) + + h.log.Error( + "failed to update bookshelf", + slog.Any("error", err), + slog.String("bookshelfID", bookshelfID), + slog.String("userID", fmt.Sprintf("%v", userID)), + ) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update bookshelf"}) return } @@ -142,19 +145,20 @@ func (h *BookshelfHandlers) UpdateBookshelf(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Bookshelf updated successfully"}) } -func (h *BookshelfHandlers) DeleteBookshelf(c *gin.Context) { - bookshelfIDHex := c.Param("id") +// Delete deletes a bookshelf. +func (h *BookshelfHandlers) Delete(c *gin.Context) { + bookshelfID := c.Param("id") - bookshelfID, err := primitive.ObjectIDFromHex(bookshelfIDHex) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid bookshelf ID"}) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - ctx := c.Request.Context() - err = h.service.DeleteBookshelf(ctx, bookshelfID) - if err != nil { - if errors.Is(err, bookshelves.ErrBookshelfNotFound) { + ctx := context.WithValue(c.Request.Context(), "userID", userID) + + if err := h.service.Delete(ctx, bookshelfID); err != nil { + if errors.Is(err, bookshelves.ErrBookshelfNotFound) || errors.Is(err, bookshelves.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/repository/bookshelf.go b/internal/server/repository/bookshelf.go index 68f233d..d97b952 100644 --- a/internal/server/repository/bookshelf.go +++ b/internal/server/repository/bookshelf.go @@ -9,7 +9,9 @@ import ( type BookshelfRepo interface { Create(ctx context.Context, bookshelf *models.Bookshelf) error GetByID(ctx context.Context, id string) (*models.Bookshelf, error) - GetByUserID(ctx context.Context, id string, page int64, limit int64) ([]*models.Bookshelf, error) + GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) // Renamed + CountByUser(ctx context.Context, userID string) (int, error) + ExistsByNameAndUser(ctx context.Context, name, userID string) (bool, error) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error Delete(ctx context.Context, id string) error } diff --git a/internal/server/services/bookshelves/bookshelves.go b/internal/server/services/bookshelves/bookshelves.go index fb82fbf..fd28da7 100644 --- a/internal/server/services/bookshelves/bookshelves.go +++ b/internal/server/services/bookshelves/bookshelves.go @@ -5,102 +5,141 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" - "time" ) +// Custom Error Types: var ( - ErrBookshelfNotFound = errors.New("bookshelf not found") + ErrBookshelfNotFound = errors.New("bookshelf not found") + ErrNameRequired = errors.New("bookshelf name is required") + ErrUserNotFoundInContext = errors.New("userID not found in context") + ErrNotAuthorized = errors.New("user is not authorized to perform this action") + ErrBookshelfAlreadyExists = errors.New("bookshelf with this name already exists for this user") ) +// BookshelfService handles business logic for bookshelves. type BookshelfService struct { - collection *mongo.Collection - log *slog.Logger + repo repository.BookshelfRepo + log *slog.Logger } -func NewBookshelfService(collection *mongo.Collection, log *slog.Logger) *BookshelfService { +// NewBookshelfService creates a new BookshelfService instance. +func NewBookshelfService(repo repository.BookshelfRepo, log *slog.Logger) *BookshelfService { return &BookshelfService{ - collection: collection, - log: log, + repo: repo, + log: log, } } -func (s *BookshelfService) CreateBookshelf(ctx context.Context, bookshelf *models.Bookshelf) (*models.Bookshelf, error) { - bookshelf.CreatedAt = time.Now() - bookshelf.UpdatedAt = time.Now() +// Create a new bookshelf. +func (s *BookshelfService) Create(ctx context.Context, bookshelf *models.Bookshelf) error { + // Rule 1: Bookshelf Name Presence + if bookshelf.Name == "" { + return ErrNameRequired + } + + // Get userID from context + userID, ok := ctx.Value("userID").(string) + if !ok { + return ErrUserNotFoundInContext + } - res, err := s.collection.InsertOne(ctx, bookshelf) + // Rule 2: Unique Bookshelf Name per User + exists, err := s.repo.ExistsByNameAndUser(ctx, bookshelf.Name, userID) if err != nil { - s.log.Error("failed to create bookshelf", slog.Any("error", err)) - return nil, fmt.Errorf("failed to create bookshelf: %w", err) + return fmt.Errorf("failed to check bookshelf existence: %w", err) + } + if exists { + return ErrBookshelfAlreadyExists } - bookshelf.ID = res.InsertedID.(primitive.ObjectID) - return bookshelf, nil + // Set the UserID for the bookshelf + bookshelf.UserID = userID + + if err := s.repo.Create(ctx, bookshelf); err != nil { + return fmt.Errorf("failed to create bookshelf: %w", err) + } + + return nil } -func (s *BookshelfService) GetBookshelf(ctx context.Context, bookshelfID primitive.ObjectID) (*models.Bookshelf, error) { - var bookshelf models.Bookshelf - err := s.collection.FindOne(ctx, bson.M{"_id": bookshelfID}).Decode(&bookshelf) +// GetByID retrieves a bookshelf by its ID. +func (s *BookshelfService) GetByID(ctx context.Context, bookshelfID string) (*models.Bookshelf, error) { + bookshelf, err := s.repo.GetByID(ctx, bookshelfID) if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { + if errors.Is(err, mongo.ErrBookshelfNotFound) { return nil, ErrBookshelfNotFound } return nil, fmt.Errorf("failed to get bookshelf: %w", err) } - return &bookshelf, nil + return bookshelf, nil } -func (s *BookshelfService) GetBookshelvesByUserID(ctx context.Context, userID primitive.ObjectID, page int64, limit int64) ([]*models.Bookshelf, error) { - findOptions := options.Find() - findOptions.SetSkip((page - 1) * limit) - findOptions.SetLimit(limit) - - cursor, err := s.collection.Find(ctx, bson.M{"user_id": userID}, findOptions) +// GetByUser retrieves a list of bookshelves for a specific user. +func (s *BookshelfService) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { + bookshelves, err := s.repo.GetByUser(ctx, userID, page, limit) if err != nil { - s.log.Error("failed to get bookshelves", slog.Any("error", err)) - return nil, fmt.Errorf("failed to get bookshelves: %w", err) - } - defer cursor.Close(ctx) - - var bookshelves []*models.Bookshelf - for cursor.Next(ctx) { - var bookshelf models.Bookshelf - if err := cursor.Decode(&bookshelf); err != nil { - return nil, fmt.Errorf("failed to decode bookshelf: %w", err) - } - bookshelves = append(bookshelves, &bookshelf) - } - - if err := cursor.Err(); err != nil { - return nil, fmt.Errorf("cursor error: %w", err) + return nil, fmt.Errorf("failed to get bookshelves by user ID: %w", err) } return bookshelves, nil } -func (s *BookshelfService) UpdateBookshelf(ctx context.Context, bookshelfID primitive.ObjectID, update bson.M) error { - update["updated_at"] = time.Now() - _, err := s.collection.UpdateOne(ctx, bson.M{"_id": bookshelfID}, bson.M{"$set": update}) +// Update updates an existing bookshelf. +func (s *BookshelfService) Update(ctx context.Context, bookshelfID string, update *models.BookshelfUpdate) error { + // Get the bookshelf + bookshelf, err := s.repo.GetByID(ctx, bookshelfID) if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { + if errors.Is(err, mongo.ErrBookshelfNotFound) { return ErrBookshelfNotFound } + return fmt.Errorf("failed to get bookshelf: %w", err) + } + + // Get userID from context + userID, ok := ctx.Value("userID").(string) + if !ok { + return ErrUserNotFoundInContext + } + + // Check bookshelf ownership + if bookshelf.UserID != userID { + return ErrNotAuthorized + } + + if err := s.repo.Update(ctx, bookshelfID, update); err != nil { return fmt.Errorf("failed to update bookshelf: %w", err) } + return nil } -func (s *BookshelfService) DeleteBookshelf(ctx context.Context, bookshelfID primitive.ObjectID) error { - _, err := s.collection.DeleteOne(ctx, bson.M{"_id": bookshelfID}) +// Delete deletes a bookshelf. +func (s *BookshelfService) Delete(ctx context.Context, bookshelfID string) error { + // Get the bookshelf + bookshelf, err := s.repo.GetByID(ctx, bookshelfID) if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { + if errors.Is(err, mongo.ErrBookshelfNotFound) { return ErrBookshelfNotFound } + return fmt.Errorf("failed to get bookshelf: %w", err) + } + + // Get userID from context + userID, ok := ctx.Value("userID").(string) + if !ok { + return ErrUserNotFoundInContext + } + + // Check bookshelf ownership + if bookshelf.UserID != userID { + return ErrNotAuthorized + } + + if err := s.repo.Delete(ctx, bookshelfID); err != nil { return fmt.Errorf("failed to delete bookshelf: %w", err) } + return nil } diff --git a/internal/server/storage/mongo/bookshelf.go b/internal/server/storage/mongo/bookshelf.go index 31db188..26d822e 100644 --- a/internal/server/storage/mongo/bookshelf.go +++ b/internal/server/storage/mongo/bookshelf.go @@ -74,8 +74,8 @@ func (r *BookshelfRepo) GetByID(ctx context.Context, id string) (*models.Bookshe return &bookshelf, nil } -// GetByUserID retrieves bookshelves associated with a specific user ID. -func (r *BookshelfRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { +// GetByUser retrieves bookshelves associated with a specific user ID. +func (r *BookshelfRepo) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { objectUserID, err := primitive.ObjectIDFromHex(userID) if err != nil { return nil, fmt.Errorf("invalid user ID: %w", err) @@ -100,6 +100,38 @@ func (r *BookshelfRepo) GetByUserID(ctx context.Context, userID string, page int return bookshelves, nil } +// CountByUser returns the number of bookshelves owned by a user. +func (r *BookshelfRepo) CountByUser(ctx context.Context, userID string) (int, error) { + objectUserID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return 0, fmt.Errorf("invalid user ID: %w", err) + } + + count, err := r.collection.CountDocuments(ctx, bson.M{"user_id": objectUserID}) + if err != nil { + r.log.Error("failed to count bookshelves by user ID", slog.Any("error", err)) + return 0, fmt.Errorf("failed to count bookshelves by user ID: %w", err) + } + + return int(count), nil +} + +// ExistsByNameAndUser checks if a bookshelf with the given name already exists for a user. +func (r *BookshelfRepo) ExistsByNameAndUser(ctx context.Context, name, userID string) (bool, error) { + objectUserID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return false, fmt.Errorf("invalid user ID: %w", err) + } + + count, err := r.collection.CountDocuments(ctx, bson.M{"name": name, "user_id": objectUserID}) + if err != nil { + r.log.Error("failed to check bookshelf existence by name and user", slog.Any("error", err)) + return false, fmt.Errorf("failed to check bookshelf existence by name and user: %w", err) + } + + return count > 0, nil +} + // Update updates a bookshelf in the database. func (r *BookshelfRepo) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error { objectID, err := primitive.ObjectIDFromHex(id) From dc409d3fece40eac87274916fa06905cd9afb949 Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 6 Jul 2024 10:55:21 +0300 Subject: [PATCH 36/56] User model finished: repo, service and handler are ready --- internal/server/handlers/users.go | 73 +++++++++++------ internal/server/repository/user.go | 1 + internal/server/services/users/users.go | 101 +++++++++++++++--------- internal/server/storage/mongo/user.go | 10 +++ 4 files changed, 124 insertions(+), 61 deletions(-) diff --git a/internal/server/handlers/users.go b/internal/server/handlers/users.go index df798e6..225edd9 100644 --- a/internal/server/handlers/users.go +++ b/internal/server/handlers/users.go @@ -1,7 +1,9 @@ package handlers import ( + "context" "errors" + "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/services/users" "github.com/gin-gonic/gin" @@ -9,11 +11,13 @@ import ( "net/http" ) +// UserHandlers handles HTTP requests related to users. type UserHandlers struct { service *users.UserService log *slog.Logger } +// NewUserHandlers creates a new UserHandlers instance. func NewUserHandlers(service *users.UserService, log *slog.Logger) *UserHandlers { return &UserHandlers{ service: service, @@ -21,8 +25,9 @@ func NewUserHandlers(service *users.UserService, log *slog.Logger) *UserHandlers } } -func (h *UserHandlers) CreateUser(c *gin.Context) { - userID, exists := c.Get("uid") +// Create creates a new user. +func (h *UserHandlers) Create(c *gin.Context) { + userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return @@ -37,10 +42,10 @@ func (h *UserHandlers) CreateUser(c *gin.Context) { user.ID = userID.(string) // Assign Firebase UID to user.ID ctx := c.Request.Context() - createdUser, err := h.service.CreateUser(ctx, &user) - if err != nil { - if errors.Is(err, users.ErrUserAlreadyExists) { - c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) + + if err := h.service.Create(ctx, &user); err != nil { + if errors.Is(err, users.ErrNotAuthorized) { + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } h.log.Error("failed to create user", slog.Any("error", err)) @@ -48,17 +53,18 @@ func (h *UserHandlers) CreateUser(c *gin.Context) { return } - c.JSON(http.StatusCreated, createdUser) + c.JSON(http.StatusCreated, user) } -func (h *UserHandlers) GetUser(c *gin.Context) { - userID := c.Param("id") // Get userID directly from the URL +// GetByID retrieves a user by ID. +func (h *UserHandlers) GetByID(c *gin.Context) { + userID := c.Param("id") ctx := c.Request.Context() - user, err := h.service.GetUserByID(ctx, userID) + user, err := h.service.GetByID(ctx, userID) if err != nil { - if errors.Is(err, users.ErrUserNotFound) { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + if errors.Is(err, users.ErrNotAuthorized) { + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } h.log.Error("failed to get user", slog.Any("error", err)) @@ -69,23 +75,35 @@ func (h *UserHandlers) GetUser(c *gin.Context) { c.JSON(http.StatusOK, user) } -func (h *UserHandlers) UpdateUser(c *gin.Context) { +// Update updates a user's information. +func (h *UserHandlers) Update(c *gin.Context) { userID := c.Param("id") - var update models.User + var update models.UserUpdate if err := c.BindJSON(&update); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - ctx := c.Request.Context() - err := h.service.UpdateUser(ctx, userID, update.ToMap()) - if err != nil { - if errors.Is(err, users.ErrUserNotFound) { + authUserID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + ctx := context.WithValue(c.Request.Context(), "userID", authUserID) + + if err := h.service.Update(ctx, userID, &update); err != nil { + if errors.Is(err, users.ErrUserNotFound) || errors.Is(err, users.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } - h.log.Error("failed to update user", slog.Any("error", err)) + h.log.Error( + "failed to update user", + slog.Any("error", err), + slog.String("userID", userID), + slog.String("authUserID", fmt.Sprintf("%v", authUserID)), + ) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) return } @@ -93,13 +111,20 @@ func (h *UserHandlers) UpdateUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"}) } -func (h *UserHandlers) DeleteUser(c *gin.Context) { +// Delete removes a user. +func (h *UserHandlers) Delete(c *gin.Context) { userID := c.Param("id") - ctx := c.Request.Context() - err := h.service.DeleteUser(ctx, userID) - if err != nil { - if errors.Is(err, users.ErrUserNotFound) { + authUserID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + ctx := context.WithValue(c.Request.Context(), "userID", authUserID) + + if err := h.service.Delete(ctx, userID); err != nil { + if errors.Is(err, users.ErrUserNotFound) || errors.Is(err, users.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/repository/user.go b/internal/server/repository/user.go index 7034542..976ccd7 100644 --- a/internal/server/repository/user.go +++ b/internal/server/repository/user.go @@ -9,6 +9,7 @@ import ( type UserRepo interface { Create(ctx context.Context, user *models.User) error GetByID(ctx context.Context, id string) (*models.User, error) + ExistsByID(ctx context.Context, id string) (bool, error) Update(ctx context.Context, id string, update *models.UserUpdate) error Delete(ctx context.Context, id string) error } diff --git a/internal/server/services/users/users.go b/internal/server/services/users/users.go index 383124e..3037f67 100644 --- a/internal/server/services/users/users.go +++ b/internal/server/services/users/users.go @@ -5,78 +5,105 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "github.com/getz-devs/librakeeper-server/internal/server/services/books" // For ErrUserNotFoundInContext + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" - "time" ) +// Custom Error Type var ( - ErrUserAlreadyExists = errors.New("user already exists") - ErrUserNotFound = errors.New("user not found") + ErrUserNotFoundInContext = errors.New("userID not found in context") + ErrNotAuthorized = errors.New("user is not authorized to perform this action") + ErrUserNotFound = errors.New("user not found") ) +// UserService handles business logic for users. type UserService struct { - collection *mongo.Collection - log *slog.Logger + repo repository.UserRepo + log *slog.Logger } -func NewUserService(collection *mongo.Collection, log *slog.Logger) *UserService { +// NewUserService creates a new UserService instance. +func NewUserService(repo repository.UserRepo, log *slog.Logger) *UserService { return &UserService{ - collection: collection, - log: log, + repo: repo, + log: log, } } -func (s *UserService) CreateUser(ctx context.Context, user *models.User) (*models.User, error) { - user.CreatedAt = time.Now() - user.UpdatedAt = time.Now() - - _, err := s.collection.InsertOne(ctx, user) +// Create a new user. +func (s *UserService) Create(ctx context.Context, user *models.User) error { + // Rule: Unique User ID + exists, err := s.repo.ExistsByID(ctx, user.ID) if err != nil { - // Check for duplicate key error (Firebase UID uniqueness) - var mongoErr mongo.WriteException - if errors.As(err, &mongoErr) && mongoErr.WriteErrors[0].Code == 11000 { - return nil, ErrUserAlreadyExists - } + return fmt.Errorf("failed to check user existence: %w", err) + } + if exists { + return mongo.ErrUserAlreadyExists + } - return nil, fmt.Errorf("failed to create user: %w", err) + if err := s.repo.Create(ctx, user); err != nil { + return fmt.Errorf("failed to create user: %w", err) } - return user, nil + return nil } -func (s *UserService) GetUserByID(ctx context.Context, userID string) (*models.User, error) { - var user models.User - err := s.collection.FindOne(ctx, bson.M{"_id": userID}).Decode(&user) +// GetByID retrieves a user by ID. +func (s *UserService) GetByID(ctx context.Context, userID string) (*models.User, error) { + user, err := s.repo.GetByID(ctx, userID) if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { + if errors.Is(err, mongo.ErrUserNotFound) { return nil, ErrUserNotFound } - return nil, fmt.Errorf("failed to get user by id: %w", err) + return nil, fmt.Errorf("failed to get user: %w", err) } - return &user, nil + return user, nil } -func (s *UserService) UpdateUser(ctx context.Context, userID string, update bson.M) error { - update["updated_at"] = time.Now() - _, err := s.collection.UpdateOne(ctx, bson.M{"_id": userID}, bson.M{"$set": update}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { +// Update updates an existing user. +func (s *UserService) Update(ctx context.Context, userID string, update *models.UserUpdate) error { + // Get userID from context (for authorization) + ctxUserID, ok := ctx.Value("userID").(string) + if !ok { + return ErrUserNotFoundInContext + } + + // Rule: User Self-Modification + if userID != ctxUserID { + return ErrNotAuthorized + } + + if err := s.repo.Update(ctx, userID, update); err != nil { + if errors.Is(err, mongo.ErrUserNotFound) { return ErrUserNotFound } return fmt.Errorf("failed to update user: %w", err) } + return nil } -func (s *UserService) DeleteUser(ctx context.Context, userID string) error { - _, err := s.collection.DeleteOne(ctx, bson.M{"_id": userID}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { +// Delete deletes a user. +func (s *UserService) Delete(ctx context.Context, userID string) error { + // Get userID from context (for authorization) + ctxUserID, ok := ctx.Value("userID").(string) + if !ok { + return books.ErrUserNotFoundInContext + } + + // Rule: User Self-Deletion + if userID != ctxUserID { + return ErrNotAuthorized + } + + if err := s.repo.Delete(ctx, userID); err != nil { + if errors.Is(err, mongo.ErrUserNotFound) { return ErrUserNotFound } return fmt.Errorf("failed to delete user: %w", err) } + return nil } diff --git a/internal/server/storage/mongo/user.go b/internal/server/storage/mongo/user.go index 0cc2c12..5d6563f 100644 --- a/internal/server/storage/mongo/user.go +++ b/internal/server/storage/mongo/user.go @@ -65,6 +65,16 @@ func (r *UserRepo) GetByID(ctx context.Context, id string) (*models.User, error) return &user, nil } +// ExistsByID checks if a user with the given ID already exists. +func (r *UserRepo) ExistsByID(ctx context.Context, id string) (bool, error) { + count, err := r.collection.CountDocuments(ctx, bson.M{"_id": id}) + if err != nil { + r.log.Error("failed to check user existence by ID", slog.Any("error", err)) + return false, fmt.Errorf("failed to check user existence by ID: %w", err) + } + return count > 0, nil +} + // Update updates a user in the database. func (r *UserRepo) Update(ctx context.Context, id string, update *models.UserUpdate) error { update.UpdatedAt = time.Now() From 0e75139ace5f436e19b9cddd8a9e3781d9a7d095 Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 6 Jul 2024 11:26:28 +0300 Subject: [PATCH 37/56] more consistent naming --- .../server/handlers/{books.go => book.go} | 52 +++++++++++-------- .../handlers/{bookshelves.go => bookshelf.go} | 32 ++++++------ .../server/handlers/{users.go => user.go} | 30 +++++------ internal/server/routes/routes.go | 34 ++++++------ .../services/{books/books.go => book/book.go} | 10 ++-- .../bookshelves.go => bookshelf/bookshelf.go} | 8 +-- internal/server/services/storage/mongo.go | 6 +-- .../services/{users/users.go => user/user.go} | 8 +-- internal/server/storage/mongo/book.go | 22 ++++---- internal/server/storage/mongo/bookshelf.go | 16 +++--- internal/server/storage/mongo/user.go | 2 +- 11 files changed, 116 insertions(+), 104 deletions(-) rename internal/server/handlers/{books.go => book.go} (75%) rename internal/server/handlers/{bookshelves.go => bookshelf.go} (80%) rename internal/server/handlers/{users.go => user.go} (80%) rename internal/server/services/{books/books.go => book/book.go} (95%) rename internal/server/services/{bookshelves/bookshelves.go => bookshelf/bookshelf.go} (94%) rename internal/server/services/{users/users.go => user/user.go} (94%) diff --git a/internal/server/handlers/books.go b/internal/server/handlers/book.go similarity index 75% rename from internal/server/handlers/books.go rename to internal/server/handlers/book.go index 427fdb3..71136b8 100644 --- a/internal/server/handlers/books.go +++ b/internal/server/handlers/book.go @@ -5,28 +5,31 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services/books" + "github.com/getz-devs/librakeeper-server/internal/server/services/book" "github.com/gin-gonic/gin" "log/slog" "net/http" "strconv" ) +// BookHandlers handles HTTP requests related to books. type BookHandlers struct { - service *books.BookService + service *book.BookService log *slog.Logger } -func NewBookHandlers(service *books.BookService, log *slog.Logger) *BookHandlers { +// NewBookHandlers creates a new BookHandlers instance. +func NewBookHandlers(service *book.BookService, log *slog.Logger) *BookHandlers { return &BookHandlers{ service: service, log: log, } } -func (h *BookHandlers) CreateBook(c *gin.Context) { - var book models.Book - if err := c.BindJSON(&book); err != nil { +// Create handles the creation of a new book. +func (h *BookHandlers) Create(c *gin.Context) { + var b models.Book + if err := c.BindJSON(&b); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -39,22 +42,23 @@ func (h *BookHandlers) CreateBook(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) - if err := h.service.Create(ctx, &book); err != nil { + if err := h.service.Create(ctx, &b); err != nil { h.log.Error("failed to create book", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) return } - c.JSON(http.StatusCreated, book) + c.JSON(http.StatusCreated, b) } -func (h *BookHandlers) GetBook(c *gin.Context) { +// GetByID retrieves a book by ID. +func (h *BookHandlers) GetByID(c *gin.Context) { bookID := c.Param("id") ctx := c.Request.Context() - book, err := h.service.GetByID(ctx, bookID) + b, err := h.service.GetByID(ctx, bookID) if err != nil { - if errors.Is(err, books.ErrBookNotFound) { + if errors.Is(err, book.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -63,10 +67,11 @@ func (h *BookHandlers) GetBook(c *gin.Context) { return } - c.JSON(http.StatusOK, book) + c.JSON(http.StatusOK, b) } -func (h *BookHandlers) GetBooksByUserID(c *gin.Context) { +// GetByUser retrieves books for a user. +func (h *BookHandlers) GetByUser(c *gin.Context) { pageStr := c.DefaultQuery("page", "1") limitStr := c.DefaultQuery("limit", "10") @@ -91,15 +96,16 @@ func (h *BookHandlers) GetBooksByUserID(c *gin.Context) { ctx := c.Request.Context() result, err := h.service.GetByUserID(ctx, userID.(string), page, limit) if err != nil { - h.log.Error("failed to get result", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result"}) + h.log.Error("failed to get books by user ID", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books by user ID"}) return } c.JSON(http.StatusOK, result) } -func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { +// GetByBookshelfID retrieves books from a specific bookshelf. +func (h *BookHandlers) GetByBookshelfID(c *gin.Context) { bookshelfID := c.Param("bookshelfId") pageStr := c.DefaultQuery("page", "1") @@ -127,15 +133,16 @@ func (h *BookHandlers) GetBooksByBookshelfID(c *gin.Context) { result, err := h.service.GetByBookshelfID(ctx, bookshelfID, page, limit) if err != nil { - h.log.Error("failed to get result by bookshelf id", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get result by bookshelf ID"}) + h.log.Error("failed to get books by bookshelf ID", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get books by bookshelf ID"}) return } c.JSON(http.StatusOK, result) } -func (h *BookHandlers) UpdateBook(c *gin.Context) { +// Update handles updating a book. +func (h *BookHandlers) Update(c *gin.Context) { bookID := c.Param("id") var update models.BookUpdate @@ -153,7 +160,7 @@ func (h *BookHandlers) UpdateBook(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) if err := h.service.Update(ctx, bookID, &update); err != nil { - if errors.Is(err, books.ErrBookNotFound) { + if errors.Is(err, book.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -171,7 +178,8 @@ func (h *BookHandlers) UpdateBook(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Book updated successfully"}) } -func (h *BookHandlers) DeleteBook(c *gin.Context) { +// Delete handles the deletion of a book. +func (h *BookHandlers) Delete(c *gin.Context) { bookID := c.Param("id") userID, exists := c.Get("userID") @@ -183,7 +191,7 @@ func (h *BookHandlers) DeleteBook(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) if err := h.service.Delete(ctx, bookID); err != nil { - if errors.Is(err, books.ErrBookNotFound) { + if errors.Is(err, book.ErrBookNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/handlers/bookshelves.go b/internal/server/handlers/bookshelf.go similarity index 80% rename from internal/server/handlers/bookshelves.go rename to internal/server/handlers/bookshelf.go index 11438b1..088c42c 100644 --- a/internal/server/handlers/bookshelves.go +++ b/internal/server/handlers/bookshelf.go @@ -5,21 +5,21 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelves" + "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelf" "github.com/gin-gonic/gin" "log/slog" "net/http" "strconv" ) -// BookshelfHandlers handles HTTP requests related to bookshelves. +// BookshelfHandlers handles HTTP requests related to bookshelf. type BookshelfHandlers struct { - service *bookshelves.BookshelfService + service *bookshelf.BookshelfService log *slog.Logger } // NewBookshelfHandlers creates a new BookshelfHandlers instance. -func NewBookshelfHandlers(service *bookshelves.BookshelfService, log *slog.Logger) *BookshelfHandlers { +func NewBookshelfHandlers(service *bookshelf.BookshelfService, log *slog.Logger) *BookshelfHandlers { return &BookshelfHandlers{ service: service, log: log, @@ -34,15 +34,15 @@ func (h *BookshelfHandlers) Create(c *gin.Context) { return } - var bookshelf models.Bookshelf - if err := c.BindJSON(&bookshelf); err != nil { + var b models.Bookshelf + if err := c.BindJSON(&b); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := context.WithValue(c.Request.Context(), "userID", userID) - if err := h.service.Create(ctx, &bookshelf); err != nil { - if errors.Is(err, bookshelves.ErrNameRequired) || errors.Is(err, bookshelves.ErrBookshelfAlreadyExists) { + if err := h.service.Create(ctx, &b); err != nil { + if errors.Is(err, bookshelf.ErrNameRequired) || errors.Is(err, bookshelf.ErrBookshelfAlreadyExists) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -52,7 +52,7 @@ func (h *BookshelfHandlers) Create(c *gin.Context) { return } - c.JSON(http.StatusCreated, bookshelf) + c.JSON(http.StatusCreated, b) } // GetByID retrieves a bookshelf by ID. @@ -60,9 +60,9 @@ func (h *BookshelfHandlers) GetByID(c *gin.Context) { bookshelfID := c.Param("id") ctx := c.Request.Context() - bookshelf, err := h.service.GetByID(ctx, bookshelfID) + b, err := h.service.GetByID(ctx, bookshelfID) if err != nil { - if errors.Is(err, bookshelves.ErrBookshelfNotFound) { + if errors.Is(err, bookshelf.ErrBookshelfNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -71,10 +71,10 @@ func (h *BookshelfHandlers) GetByID(c *gin.Context) { return } - c.JSON(http.StatusOK, bookshelf) + c.JSON(http.StatusOK, b) } -// GetByUser retrieves bookshelves for a user. +// GetByUser retrieves bookshelf for a user. func (h *BookshelfHandlers) GetByUser(c *gin.Context) { userID, exists := c.Get("userID") if !exists { @@ -101,7 +101,7 @@ func (h *BookshelfHandlers) GetByUser(c *gin.Context) { result, err := h.service.GetByUser(ctx, userID.(string), page, limit) if err != nil { h.log.Error("failed to get result by user id", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelves by user ID"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelf by user ID"}) return } @@ -127,7 +127,7 @@ func (h *BookshelfHandlers) Update(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) if err := h.service.Update(ctx, bookshelfID, &update); err != nil { - if errors.Is(err, bookshelves.ErrBookshelfNotFound) || errors.Is(err, bookshelves.ErrNotAuthorized) { + if errors.Is(err, bookshelf.ErrBookshelfNotFound) || errors.Is(err, bookshelf.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -158,7 +158,7 @@ func (h *BookshelfHandlers) Delete(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) if err := h.service.Delete(ctx, bookshelfID); err != nil { - if errors.Is(err, bookshelves.ErrBookshelfNotFound) || errors.Is(err, bookshelves.ErrNotAuthorized) { + if errors.Is(err, bookshelf.ErrBookshelfNotFound) || errors.Is(err, bookshelf.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/handlers/users.go b/internal/server/handlers/user.go similarity index 80% rename from internal/server/handlers/users.go rename to internal/server/handlers/user.go index 225edd9..1d51549 100644 --- a/internal/server/handlers/users.go +++ b/internal/server/handlers/user.go @@ -5,20 +5,20 @@ import ( "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services/users" + "github.com/getz-devs/librakeeper-server/internal/server/services/user" "github.com/gin-gonic/gin" "log/slog" "net/http" ) -// UserHandlers handles HTTP requests related to users. +// UserHandlers handles HTTP requests related to user. type UserHandlers struct { - service *users.UserService + service *user.UserService log *slog.Logger } // NewUserHandlers creates a new UserHandlers instance. -func NewUserHandlers(service *users.UserService, log *slog.Logger) *UserHandlers { +func NewUserHandlers(service *user.UserService, log *slog.Logger) *UserHandlers { return &UserHandlers{ service: service, log: log, @@ -33,18 +33,18 @@ func (h *UserHandlers) Create(c *gin.Context) { return } - var user models.User - if err := c.BindJSON(&user); err != nil { + var u models.User + if err := c.BindJSON(&u); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - user.ID = userID.(string) // Assign Firebase UID to user.ID + u.ID = userID.(string) // Assign Firebase UID to user.ID ctx := c.Request.Context() - if err := h.service.Create(ctx, &user); err != nil { - if errors.Is(err, users.ErrNotAuthorized) { + if err := h.service.Create(ctx, &u); err != nil { + if errors.Is(err, user.ErrNotAuthorized) { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } @@ -53,7 +53,7 @@ func (h *UserHandlers) Create(c *gin.Context) { return } - c.JSON(http.StatusCreated, user) + c.JSON(http.StatusCreated, u) } // GetByID retrieves a user by ID. @@ -61,9 +61,9 @@ func (h *UserHandlers) GetByID(c *gin.Context) { userID := c.Param("id") ctx := c.Request.Context() - user, err := h.service.GetByID(ctx, userID) + u, err := h.service.GetByID(ctx, userID) if err != nil { - if errors.Is(err, users.ErrNotAuthorized) { + if errors.Is(err, user.ErrNotAuthorized) { c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } @@ -72,7 +72,7 @@ func (h *UserHandlers) GetByID(c *gin.Context) { return } - c.JSON(http.StatusOK, user) + c.JSON(http.StatusOK, u) } // Update updates a user's information. @@ -94,7 +94,7 @@ func (h *UserHandlers) Update(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", authUserID) if err := h.service.Update(ctx, userID, &update); err != nil { - if errors.Is(err, users.ErrUserNotFound) || errors.Is(err, users.ErrNotAuthorized) { + if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } @@ -124,7 +124,7 @@ func (h *UserHandlers) Delete(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", authUserID) if err := h.service.Delete(ctx, userID); err != nil { - if errors.Is(err, users.ErrUserNotFound) || errors.Is(err, users.ErrNotAuthorized) { + if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrNotAuthorized) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index b0fdd07..7de7210 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -13,35 +13,39 @@ type Handlers struct { Books *handlers.BookHandlers } +// SetupRoutes sets up the API routes for the server. func SetupRoutes(router *gin.Engine, h *Handlers) { api := router.Group("/api") api.GET("/health", handlers.HealthCheck) + // User routes userGroup := api.Group("/users") { - userGroup.POST("/", middlewares.AuthMiddleware(), h.Users.CreateUser) - userGroup.GET("/:id", middlewares.AuthMiddleware(), h.Users.GetUser) - userGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Users.UpdateUser) - userGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Users.DeleteUser) + userGroup.POST("/", middlewares.AuthMiddleware(), h.Users.Create) + userGroup.GET("/:id", middlewares.AuthMiddleware(), h.Users.GetByID) + userGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Users.Update) + userGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Users.Delete) } + // Bookshelf routes bookshelvesGroup := api.Group("/bookshelves") { - bookshelvesGroup.POST("/", middlewares.AuthMiddleware(), h.Bookshelves.CreateBookshelf) - bookshelvesGroup.GET("/:id", h.Bookshelves.GetBookshelf) - bookshelvesGroup.GET("/user", middlewares.AuthMiddleware(), h.Bookshelves.GetBookshelvesByUserID) - bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.UpdateBookshelf) - bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.DeleteBookshelf) + bookshelvesGroup.POST("/", middlewares.AuthMiddleware(), h.Bookshelves.Create) + bookshelvesGroup.GET("/:id", middlewares.AuthMiddleware(), h.Bookshelves.GetByID) + bookshelvesGroup.GET("/user", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) + bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Update) + bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Delete) } + // Book routes booksGroup := api.Group("/books") { - booksGroup.POST("/", h.Books.CreateBook) - booksGroup.GET("/:id", h.Books.GetBook) - booksGroup.GET("/", h.Books.GetBooks) - booksGroup.GET("/bookshelf/:bookshelfId", h.Books.GetBooksByBookshelfID) - booksGroup.PUT("/:id", h.Books.UpdateBook) - booksGroup.DELETE("/:id", h.Books.DeleteBook) + booksGroup.POST("/", middlewares.AuthMiddleware(), h.Books.Create) + booksGroup.GET("/:id", h.Books.GetByID) + booksGroup.GET("/user", middlewares.AuthMiddleware(), h.Books.GetByUser) + booksGroup.GET("/bookshelf/:bookshelfId", middlewares.AuthMiddleware(), h.Books.GetByBookshelfID) + booksGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Books.Update) + booksGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Books.Delete) } } diff --git a/internal/server/services/books/books.go b/internal/server/services/book/book.go similarity index 95% rename from internal/server/services/books/books.go rename to internal/server/services/book/book.go index fcee459..b15c61c 100644 --- a/internal/server/services/books/books.go +++ b/internal/server/services/book/book.go @@ -1,4 +1,4 @@ -package books +package book import ( "context" @@ -101,20 +101,20 @@ func (s *BookService) GetByID(ctx context.Context, bookID string) (*models.Book, return book, nil } -// GetByUserID retrieves a list of books for a specific user. +// GetByUserID retrieves a list of book for a specific user. func (s *BookService) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { books, err := s.repo.GetByUserID(ctx, userID, page, limit) if err != nil { - return nil, fmt.Errorf("failed to get books by user ID: %w", err) + return nil, fmt.Errorf("failed to get book by user ID: %w", err) } return books, nil } -// GetByBookshelfID retrieves books by bookshelf ID. +// GetByBookshelfID retrieves book by bookshelf ID. func (s *BookService) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { books, err := s.repo.GetByBookshelfID(ctx, bookshelfID, page, limit) if err != nil { - return nil, fmt.Errorf("failed to get books by bookshelf ID: %w", err) + return nil, fmt.Errorf("failed to get book by bookshelf ID: %w", err) } return books, nil } diff --git a/internal/server/services/bookshelves/bookshelves.go b/internal/server/services/bookshelf/bookshelf.go similarity index 94% rename from internal/server/services/bookshelves/bookshelves.go rename to internal/server/services/bookshelf/bookshelf.go index fd28da7..e396bab 100644 --- a/internal/server/services/bookshelves/bookshelves.go +++ b/internal/server/services/bookshelf/bookshelf.go @@ -1,4 +1,4 @@ -package bookshelves +package bookshelf import ( "context" @@ -19,7 +19,7 @@ var ( ErrBookshelfAlreadyExists = errors.New("bookshelf with this name already exists for this user") ) -// BookshelfService handles business logic for bookshelves. +// BookshelfService handles business logic for bookshelf. type BookshelfService struct { repo repository.BookshelfRepo log *slog.Logger @@ -77,11 +77,11 @@ func (s *BookshelfService) GetByID(ctx context.Context, bookshelfID string) (*mo return bookshelf, nil } -// GetByUser retrieves a list of bookshelves for a specific user. +// GetByUser retrieves a list of bookshelf for a specific user. func (s *BookshelfService) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { bookshelves, err := s.repo.GetByUser(ctx, userID, page, limit) if err != nil { - return nil, fmt.Errorf("failed to get bookshelves by user ID: %w", err) + return nil, fmt.Errorf("failed to get bookshelf by user ID: %w", err) } return bookshelves, nil } diff --git a/internal/server/services/storage/mongo.go b/internal/server/services/storage/mongo.go index 77e768b..134397b 100644 --- a/internal/server/services/storage/mongo.go +++ b/internal/server/services/storage/mongo.go @@ -37,9 +37,9 @@ func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, Collecti _db = client.Database(cfg.Database.Name) _log.Info("connected to mongodb", slog.String("database", cfg.Database.Name)) - _collections.UsersCollection = _db.Collection("users") - _collections.BooksCollection = _db.Collection("books") - _collections.BookshelvesCollection = _db.Collection("bookshelves") + _collections.UsersCollection = _db.Collection("user") + _collections.BooksCollection = _db.Collection("book") + _collections.BookshelvesCollection = _db.Collection("bookshelf") return _db, _collections } diff --git a/internal/server/services/users/users.go b/internal/server/services/user/user.go similarity index 94% rename from internal/server/services/users/users.go rename to internal/server/services/user/user.go index 3037f67..be31408 100644 --- a/internal/server/services/users/users.go +++ b/internal/server/services/user/user.go @@ -1,4 +1,4 @@ -package users +package user import ( "context" @@ -6,7 +6,7 @@ import ( "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/repository" - "github.com/getz-devs/librakeeper-server/internal/server/services/books" // For ErrUserNotFoundInContext + "github.com/getz-devs/librakeeper-server/internal/server/services/book" // For ErrUserNotFoundInContext "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" ) @@ -18,7 +18,7 @@ var ( ErrUserNotFound = errors.New("user not found") ) -// UserService handles business logic for users. +// UserService handles business logic for user. type UserService struct { repo repository.UserRepo log *slog.Logger @@ -90,7 +90,7 @@ func (s *UserService) Delete(ctx context.Context, userID string) error { // Get userID from context (for authorization) ctxUserID, ok := ctx.Value("userID").(string) if !ok { - return books.ErrUserNotFoundInContext + return book.ErrUserNotFoundInContext } // Rule: User Self-Deletion diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index 09acf1b..d48930e 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -29,7 +29,7 @@ type BookRepo struct { // NewBookRepo creates a new BookRepo instance. func NewBookRepo(db *mongo.Database, log *slog.Logger) repository.BookRepo { return &BookRepo{ - collection: db.Collection("books"), // Note: Collection name corrected to "books" + collection: db.Collection("book"), // Note: Collection name corrected to "book" log: log, } } @@ -73,7 +73,7 @@ func (r *BookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) return &book, nil } -// GetByUserID retrieves books associated with a specific user ID. +// GetByUserID retrieves book associated with a specific user ID. func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { objectUserID, err := primitive.ObjectIDFromHex(userID) if err != nil { @@ -86,19 +86,19 @@ func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, l cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) if err != nil { - return nil, fmt.Errorf("failed to get books by user ID: %w", err) + return nil, fmt.Errorf("failed to get book by user ID: %w", err) } defer cursor.Close(ctx) var books []*models.Book if err = cursor.All(ctx, &books); err != nil { - return nil, fmt.Errorf("failed to decode books: %w", err) + return nil, fmt.Errorf("failed to decode book: %w", err) } return books, nil } -// GetByBookshelfID retrieves books belonging to a specific bookshelf ID. +// GetByBookshelfID retrieves book belonging to a specific bookshelf ID. func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) if err != nil { @@ -111,20 +111,20 @@ func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, pag cursor, err := r.collection.Aggregate(ctx, mongo.Pipeline{matchStage, skipStage, limitStage}) if err != nil { - r.log.Error("failed to get books by bookshelf id", slog.Any("error", err)) - return nil, fmt.Errorf("failed to get books by bookshelf id: %w", err) + r.log.Error("failed to get book by bookshelf id", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get book by bookshelf id: %w", err) } defer cursor.Close(ctx) var books []*models.Book if err = cursor.All(ctx, &books); err != nil { - return nil, fmt.Errorf("failed to decode books: %w", err) + return nil, fmt.Errorf("failed to decode book: %w", err) } return books, nil } -// CountInBookshelf returns the number of books in a bookshelf. +// CountInBookshelf returns the number of book in a bookshelf. func (r *BookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) { objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) if err != nil { @@ -133,8 +133,8 @@ func (r *BookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (in count, err := r.collection.CountDocuments(ctx, bson.M{"bookshelf_id": objectBookshelfID}) if err != nil { - r.log.Error("failed to count books by bookshelf ID", slog.Any("error", err)) - return 0, fmt.Errorf("failed to count books by bookshelf ID: %w", err) + r.log.Error("failed to count book by bookshelf ID", slog.Any("error", err)) + return 0, fmt.Errorf("failed to count book by bookshelf ID: %w", err) } return int(count), nil diff --git a/internal/server/storage/mongo/bookshelf.go b/internal/server/storage/mongo/bookshelf.go index 26d822e..a34a3f7 100644 --- a/internal/server/storage/mongo/bookshelf.go +++ b/internal/server/storage/mongo/bookshelf.go @@ -29,7 +29,7 @@ type BookshelfRepo struct { // NewBookshelfRepo creates a new BookshelfRepo instance. func NewBookshelfRepo(db *mongo.Database, log *slog.Logger) repository.BookshelfRepo { return &BookshelfRepo{ - collection: db.Collection("bookshelves"), + collection: db.Collection("bookshelf"), log: log, } } @@ -74,7 +74,7 @@ func (r *BookshelfRepo) GetByID(ctx context.Context, id string) (*models.Bookshe return &bookshelf, nil } -// GetByUser retrieves bookshelves associated with a specific user ID. +// GetByUser retrieves bookshelf associated with a specific user ID. func (r *BookshelfRepo) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { objectUserID, err := primitive.ObjectIDFromHex(userID) if err != nil { @@ -87,20 +87,20 @@ func (r *BookshelfRepo) GetByUser(ctx context.Context, userID string, page int64 cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) if err != nil { - r.log.Error("failed to get bookshelves by user ID", slog.Any("error", err)) - return nil, fmt.Errorf("failed to get bookshelves by user ID: %w", err) + r.log.Error("failed to get bookshelf by user ID", slog.Any("error", err)) + return nil, fmt.Errorf("failed to get bookshelf by user ID: %w", err) } defer cursor.Close(ctx) var bookshelves []*models.Bookshelf if err = cursor.All(ctx, &bookshelves); err != nil { - return nil, fmt.Errorf("failed to decode bookshelves: %w", err) + return nil, fmt.Errorf("failed to decode bookshelf: %w", err) } return bookshelves, nil } -// CountByUser returns the number of bookshelves owned by a user. +// CountByUser returns the number of bookshelf owned by a user. func (r *BookshelfRepo) CountByUser(ctx context.Context, userID string) (int, error) { objectUserID, err := primitive.ObjectIDFromHex(userID) if err != nil { @@ -109,8 +109,8 @@ func (r *BookshelfRepo) CountByUser(ctx context.Context, userID string) (int, er count, err := r.collection.CountDocuments(ctx, bson.M{"user_id": objectUserID}) if err != nil { - r.log.Error("failed to count bookshelves by user ID", slog.Any("error", err)) - return 0, fmt.Errorf("failed to count bookshelves by user ID: %w", err) + r.log.Error("failed to count bookshelf by user ID", slog.Any("error", err)) + return 0, fmt.Errorf("failed to count bookshelf by user ID: %w", err) } return int(count), nil diff --git a/internal/server/storage/mongo/user.go b/internal/server/storage/mongo/user.go index 5d6563f..d717812 100644 --- a/internal/server/storage/mongo/user.go +++ b/internal/server/storage/mongo/user.go @@ -27,7 +27,7 @@ type UserRepo struct { // NewUserRepo creates a new UserRepo instance. func NewUserRepo(db *mongo.Database, log *slog.Logger) repository.UserRepo { return &UserRepo{ - collection: db.Collection("users"), + collection: db.Collection("user"), log: log, } } From 99b1b946bb146fcf966494dc5440d5c3b84475f9 Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 6 Jul 2024 12:12:03 +0300 Subject: [PATCH 38/56] final changes: repos used in the server --- .../server/{services => }/auth/firebase.go | 0 internal/server/middlewares/auth.go | 8 +++--- internal/server/server.go | 24 +++++++++++------ internal/server/services/storage/mongo.go | 27 +++++-------------- 4 files changed, 26 insertions(+), 33 deletions(-) rename internal/server/{services => }/auth/firebase.go (100%) diff --git a/internal/server/services/auth/firebase.go b/internal/server/auth/firebase.go similarity index 100% rename from internal/server/services/auth/firebase.go rename to internal/server/auth/firebase.go diff --git a/internal/server/middlewares/auth.go b/internal/server/middlewares/auth.go index 7cc249a..770f880 100644 --- a/internal/server/middlewares/auth.go +++ b/internal/server/middlewares/auth.go @@ -2,7 +2,7 @@ package middlewares import ( "context" - "github.com/getz-devs/librakeeper-server/internal/server/services/auth" + "github.com/getz-devs/librakeeper-server/internal/server/auth" "net/http" "strings" @@ -35,10 +35,8 @@ func AuthMiddleware() gin.HandlerFunc { return } - // Add the UID to the context for further use. - ctx := context.WithValue(c.Request.Context(), "userID", token.UID) - c.Request = c.Request.WithContext(ctx) // Update the request's context - + // Set the UID in the context for further use + c.Set("userID", token.UID) c.Next() } } diff --git a/internal/server/server.go b/internal/server/server.go index 8d611f0..c1834a3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,14 +4,15 @@ import ( "context" "errors" "fmt" + "github.com/getz-devs/librakeeper-server/internal/server/auth" "github.com/getz-devs/librakeeper-server/internal/server/config" "github.com/getz-devs/librakeeper-server/internal/server/handlers" "github.com/getz-devs/librakeeper-server/internal/server/routes" - "github.com/getz-devs/librakeeper-server/internal/server/services/auth" - "github.com/getz-devs/librakeeper-server/internal/server/services/books" - "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelves" + "github.com/getz-devs/librakeeper-server/internal/server/services/book" + "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelf" "github.com/getz-devs/librakeeper-server/internal/server/services/storage" - "github.com/getz-devs/librakeeper-server/internal/server/services/users" + "github.com/getz-devs/librakeeper-server/internal/server/services/user" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "log/slog" @@ -47,11 +48,18 @@ func (s *Server) Initialize() error { return fmt.Errorf("failed to initialize Firebase: %w", err) } - _, collections := storage.Initialize(s.config, s.log) + db, err := storage.Initialize(s.config, s.log) + if err != nil { + return fmt.Errorf("failed to initialize Database: %w", err) + } + + userRepo := mongo.NewUserRepo(db, s.log) + bookRepo := mongo.NewBookRepo(db, s.log) + bookshelfRepo := mongo.NewBookshelfRepo(db, s.log) - userService := users.NewUserService(collections.UsersCollection, s.log) - bookshelfService := bookshelves.NewBookshelfService(collections.BookshelvesCollection, s.log) - bookService := books.NewBookService(collections.BooksCollection, s.log) + userService := user.NewUserService(userRepo, s.log) + bookService := book.NewBookService(bookRepo, bookshelfRepo, s.log) + bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) h := &routes.Handlers{ Users: handlers.NewUserHandlers(userService, s.log), diff --git a/internal/server/services/storage/mongo.go b/internal/server/services/storage/mongo.go index 134397b..57f29b6 100644 --- a/internal/server/services/storage/mongo.go +++ b/internal/server/services/storage/mongo.go @@ -2,6 +2,7 @@ package storage import ( "context" + "errors" "fmt" "github.com/getz-devs/librakeeper-server/internal/server/config" "log/slog" @@ -11,37 +12,23 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) -type Collections struct { - UsersCollection *mongo.Collection - BooksCollection *mongo.Collection - BookshelvesCollection *mongo.Collection -} - var ( - _log *slog.Logger - _db *mongo.Database - _collections Collections + _db *mongo.Database ) -func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, Collections) { - _log = log +func Initialize(cfg *config.Config, log *slog.Logger) (*mongo.Database, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() client, err := mongo.Connect(ctx, options.Client().ApplyURI(cfg.Database.URI)) if err != nil { - _log.Error("failed to connect to mongodb", slog.Any("error", err)) - panic(err) + return nil, errors.New("failed to connect to mongodb") } _db = client.Database(cfg.Database.Name) - _log.Info("connected to mongodb", slog.String("database", cfg.Database.Name)) - - _collections.UsersCollection = _db.Collection("user") - _collections.BooksCollection = _db.Collection("book") - _collections.BookshelvesCollection = _db.Collection("bookshelf") + log.Info("connected to mongodb", slog.String("database", cfg.Database.Name)) - return _db, _collections + return _db, nil } // Ping checks the database connectivity. @@ -50,7 +37,7 @@ func Ping(ctx context.Context) error { defer cancel() if _db == nil { - panic(fmt.Errorf("mongodb has not been initialized")) + return errors.New("mongodb has not been initialized") } if err := _db.Client().Ping(ctx, nil); err != nil { From 7baeb66490d7efb3b4da456c83f7b0d0bcdf0f15 Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 13 Jul 2024 19:28:21 +0300 Subject: [PATCH 39/56] remove User-related stuff --- internal/server/handlers/book.go | 2 +- internal/server/handlers/user.go | 137 --------------------- internal/server/models/user.go | 19 --- internal/server/repository/user.go | 15 --- internal/server/routes/routes.go | 20 +-- internal/server/services/user/user.go | 109 ---------------- internal/server/storage/mongo/book.go | 49 ++------ internal/server/storage/mongo/bookshelf.go | 43 ++----- internal/server/storage/mongo/user.go | 101 --------------- 9 files changed, 20 insertions(+), 475 deletions(-) delete mode 100644 internal/server/handlers/user.go delete mode 100644 internal/server/models/user.go delete mode 100644 internal/server/repository/user.go delete mode 100644 internal/server/services/user/user.go delete mode 100644 internal/server/storage/mongo/user.go diff --git a/internal/server/handlers/book.go b/internal/server/handlers/book.go index 71136b8..1d69fda 100644 --- a/internal/server/handlers/book.go +++ b/internal/server/handlers/book.go @@ -106,7 +106,7 @@ func (h *BookHandlers) GetByUser(c *gin.Context) { // GetByBookshelfID retrieves books from a specific bookshelf. func (h *BookHandlers) GetByBookshelfID(c *gin.Context) { - bookshelfID := c.Param("bookshelfId") + bookshelfID := c.Param("id") pageStr := c.DefaultQuery("page", "1") limitStr := c.DefaultQuery("limit", "10") diff --git a/internal/server/handlers/user.go b/internal/server/handlers/user.go deleted file mode 100644 index 1d51549..0000000 --- a/internal/server/handlers/user.go +++ /dev/null @@ -1,137 +0,0 @@ -package handlers - -import ( - "context" - "errors" - "fmt" - "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/services/user" - "github.com/gin-gonic/gin" - "log/slog" - "net/http" -) - -// UserHandlers handles HTTP requests related to user. -type UserHandlers struct { - service *user.UserService - log *slog.Logger -} - -// NewUserHandlers creates a new UserHandlers instance. -func NewUserHandlers(service *user.UserService, log *slog.Logger) *UserHandlers { - return &UserHandlers{ - service: service, - log: log, - } -} - -// Create creates a new user. -func (h *UserHandlers) Create(c *gin.Context) { - userID, exists := c.Get("userID") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - var u models.User - if err := c.BindJSON(&u); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - u.ID = userID.(string) // Assign Firebase UID to user.ID - - ctx := c.Request.Context() - - if err := h.service.Create(ctx, &u); err != nil { - if errors.Is(err, user.ErrNotAuthorized) { - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) - return - } - h.log.Error("failed to create user", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) - return - } - - c.JSON(http.StatusCreated, u) -} - -// GetByID retrieves a user by ID. -func (h *UserHandlers) GetByID(c *gin.Context) { - userID := c.Param("id") - - ctx := c.Request.Context() - u, err := h.service.GetByID(ctx, userID) - if err != nil { - if errors.Is(err, user.ErrNotAuthorized) { - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) - return - } - h.log.Error("failed to get user", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"}) - return - } - - c.JSON(http.StatusOK, u) -} - -// Update updates a user's information. -func (h *UserHandlers) Update(c *gin.Context) { - userID := c.Param("id") - - var update models.UserUpdate - if err := c.BindJSON(&update); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - authUserID, exists := c.Get("userID") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - ctx := context.WithValue(c.Request.Context(), "userID", authUserID) - - if err := h.service.Update(ctx, userID, &update); err != nil { - if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrNotAuthorized) { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - h.log.Error( - "failed to update user", - slog.Any("error", err), - slog.String("userID", userID), - slog.String("authUserID", fmt.Sprintf("%v", authUserID)), - ) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"}) -} - -// Delete removes a user. -func (h *UserHandlers) Delete(c *gin.Context) { - userID := c.Param("id") - - authUserID, exists := c.Get("userID") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - return - } - - ctx := context.WithValue(c.Request.Context(), "userID", authUserID) - - if err := h.service.Delete(ctx, userID); err != nil { - if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, user.ErrNotAuthorized) { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - h.log.Error("failed to delete user", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) -} diff --git a/internal/server/models/user.go b/internal/server/models/user.go deleted file mode 100644 index 4a98cad..0000000 --- a/internal/server/models/user.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "time" -) - -// User represents a user in the system. -type User struct { - ID string `bson:"_id,omitempty" json:"id"` // Firebase UID as primary key - DisplayName string `bson:"display_name" json:"display_name"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` -} - -// UserUpdate represents fields that can be updated in a User. -type UserUpdate struct { - DisplayName *string `bson:"display_name,omitempty" json:"display_name,omitempty"` // Optional field for update - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` -} diff --git a/internal/server/repository/user.go b/internal/server/repository/user.go deleted file mode 100644 index 976ccd7..0000000 --- a/internal/server/repository/user.go +++ /dev/null @@ -1,15 +0,0 @@ -package repository - -import ( - "context" - "github.com/getz-devs/librakeeper-server/internal/server/models" -) - -// UserRepo defines the interface for user repository operations. -type UserRepo interface { - Create(ctx context.Context, user *models.User) error - GetByID(ctx context.Context, id string) (*models.User, error) - ExistsByID(ctx context.Context, id string) (bool, error) - Update(ctx context.Context, id string, update *models.UserUpdate) error - Delete(ctx context.Context, id string) error -} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 7de7210..a9448b6 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -8,7 +8,6 @@ import ( // Handlers is a struct that groups all handler functions. type Handlers struct { - Users *handlers.UserHandlers Bookshelves *handlers.BookshelfHandlers Books *handlers.BookHandlers } @@ -19,21 +18,12 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { api.GET("/health", handlers.HealthCheck) - // User routes - userGroup := api.Group("/users") - { - userGroup.POST("/", middlewares.AuthMiddleware(), h.Users.Create) - userGroup.GET("/:id", middlewares.AuthMiddleware(), h.Users.GetByID) - userGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Users.Update) - userGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Users.Delete) - } - // Bookshelf routes bookshelvesGroup := api.Group("/bookshelves") { - bookshelvesGroup.POST("/", middlewares.AuthMiddleware(), h.Bookshelves.Create) + bookshelvesGroup.GET("/", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) bookshelvesGroup.GET("/:id", middlewares.AuthMiddleware(), h.Bookshelves.GetByID) - bookshelvesGroup.GET("/user", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) + bookshelvesGroup.POST("/add", middlewares.AuthMiddleware(), h.Bookshelves.Create) bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Update) bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Delete) } @@ -41,10 +31,10 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { // Book routes booksGroup := api.Group("/books") { - booksGroup.POST("/", middlewares.AuthMiddleware(), h.Books.Create) + booksGroup.GET("/", middlewares.AuthMiddleware(), h.Books.GetByUser) booksGroup.GET("/:id", h.Books.GetByID) - booksGroup.GET("/user", middlewares.AuthMiddleware(), h.Books.GetByUser) - booksGroup.GET("/bookshelf/:bookshelfId", middlewares.AuthMiddleware(), h.Books.GetByBookshelfID) + booksGroup.GET("/bookshelf/:id", middlewares.AuthMiddleware(), h.Books.GetByBookshelfID) + booksGroup.POST("/", middlewares.AuthMiddleware(), h.Books.Create) booksGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Books.Update) booksGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Books.Delete) } diff --git a/internal/server/services/user/user.go b/internal/server/services/user/user.go deleted file mode 100644 index be31408..0000000 --- a/internal/server/services/user/user.go +++ /dev/null @@ -1,109 +0,0 @@ -package user - -import ( - "context" - "errors" - "fmt" - "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/repository" - "github.com/getz-devs/librakeeper-server/internal/server/services/book" // For ErrUserNotFoundInContext - "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" - "log/slog" -) - -// Custom Error Type -var ( - ErrUserNotFoundInContext = errors.New("userID not found in context") - ErrNotAuthorized = errors.New("user is not authorized to perform this action") - ErrUserNotFound = errors.New("user not found") -) - -// UserService handles business logic for user. -type UserService struct { - repo repository.UserRepo - log *slog.Logger -} - -// NewUserService creates a new UserService instance. -func NewUserService(repo repository.UserRepo, log *slog.Logger) *UserService { - return &UserService{ - repo: repo, - log: log, - } -} - -// Create a new user. -func (s *UserService) Create(ctx context.Context, user *models.User) error { - // Rule: Unique User ID - exists, err := s.repo.ExistsByID(ctx, user.ID) - if err != nil { - return fmt.Errorf("failed to check user existence: %w", err) - } - if exists { - return mongo.ErrUserAlreadyExists - } - - if err := s.repo.Create(ctx, user); err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - - return nil -} - -// GetByID retrieves a user by ID. -func (s *UserService) GetByID(ctx context.Context, userID string) (*models.User, error) { - user, err := s.repo.GetByID(ctx, userID) - if err != nil { - if errors.Is(err, mongo.ErrUserNotFound) { - return nil, ErrUserNotFound - } - return nil, fmt.Errorf("failed to get user: %w", err) - } - return user, nil -} - -// Update updates an existing user. -func (s *UserService) Update(ctx context.Context, userID string, update *models.UserUpdate) error { - // Get userID from context (for authorization) - ctxUserID, ok := ctx.Value("userID").(string) - if !ok { - return ErrUserNotFoundInContext - } - - // Rule: User Self-Modification - if userID != ctxUserID { - return ErrNotAuthorized - } - - if err := s.repo.Update(ctx, userID, update); err != nil { - if errors.Is(err, mongo.ErrUserNotFound) { - return ErrUserNotFound - } - return fmt.Errorf("failed to update user: %w", err) - } - - return nil -} - -// Delete deletes a user. -func (s *UserService) Delete(ctx context.Context, userID string) error { - // Get userID from context (for authorization) - ctxUserID, ok := ctx.Value("userID").(string) - if !ok { - return book.ErrUserNotFoundInContext - } - - // Rule: User Self-Deletion - if userID != ctxUserID { - return ErrNotAuthorized - } - - if err := s.repo.Delete(ctx, userID); err != nil { - if errors.Is(err, mongo.ErrUserNotFound) { - return ErrUserNotFound - } - return fmt.Errorf("failed to delete user: %w", err) - } - - return nil -} diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index d48930e..3843eb2 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -57,13 +57,8 @@ func (r *BookRepo) Create(ctx context.Context, book *models.Book) error { // GetByID retrieves a book from the database by its ID. func (r *BookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return nil, fmt.Errorf("invalid book ID: %w", err) - } - var book models.Book - err = r.collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&book) + err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&book) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return nil, ErrBookNotFound @@ -75,16 +70,11 @@ func (r *BookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) // GetByUserID retrieves book associated with a specific user ID. func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { - objectUserID, err := primitive.ObjectIDFromHex(userID) - if err != nil { - return nil, fmt.Errorf("invalid user ID: %w", err) - } - findOptions := options.Find() findOptions.SetSkip((page - 1) * limit) findOptions.SetLimit(limit) - cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) + cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID}, findOptions) if err != nil { return nil, fmt.Errorf("failed to get book by user ID: %w", err) } @@ -100,12 +90,7 @@ func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, l // GetByBookshelfID retrieves book belonging to a specific bookshelf ID. func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { - objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) - if err != nil { - return nil, fmt.Errorf("invalid bookshelf ID: %w", err) - } - - matchStage := bson.D{{"$match", bson.D{{"bookshelf_id", objectBookshelfID}}}} + matchStage := bson.D{{"$match", bson.D{{"bookshelf_id", bookshelfID}}}} skipStage := bson.D{{"$skip", (page - 1) * limit}} limitStage := bson.D{{"$limit", limit}} @@ -126,12 +111,7 @@ func (r *BookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, pag // CountInBookshelf returns the number of book in a bookshelf. func (r *BookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) { - objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) - if err != nil { - return 0, fmt.Errorf("invalid bookshelf ID: %w", err) - } - - count, err := r.collection.CountDocuments(ctx, bson.M{"bookshelf_id": objectBookshelfID}) + count, err := r.collection.CountDocuments(ctx, bson.M{"bookshelf_id": bookshelfID}) if err != nil { r.log.Error("failed to count book by bookshelf ID", slog.Any("error", err)) return 0, fmt.Errorf("failed to count book by bookshelf ID: %w", err) @@ -142,12 +122,7 @@ func (r *BookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (in // ExistsInBookshelf checks if a book with the given ISBN already exists in the bookshelf. func (r *BookRepo) ExistsInBookshelf(ctx context.Context, isbn, bookshelfID string) (bool, error) { - objectBookshelfID, err := primitive.ObjectIDFromHex(bookshelfID) - if err != nil { - return false, fmt.Errorf("invalid bookshelf ID: %w", err) - } - - count, err := r.collection.CountDocuments(ctx, bson.M{"isbn": isbn, "bookshelf_id": objectBookshelfID}) + count, err := r.collection.CountDocuments(ctx, bson.M{"isbn": isbn, "bookshelf_id": bookshelfID}) if err != nil { r.log.Error("failed to check book existence", slog.Any("error", err)) return false, fmt.Errorf("failed to check book existence: %w", err) @@ -158,13 +133,8 @@ func (r *BookRepo) ExistsInBookshelf(ctx context.Context, isbn, bookshelfID stri // Update updates a book in the database. func (r *BookRepo) Update(ctx context.Context, id string, update *models.BookUpdate) error { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return fmt.Errorf("invalid book ID: %w", err) - } - update.UpdatedAt = time.Now() - _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) + _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookNotFound @@ -176,12 +146,7 @@ func (r *BookRepo) Update(ctx context.Context, id string, update *models.BookUpd // Delete removes a book from the database. func (r *BookRepo) Delete(ctx context.Context, id string) error { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return fmt.Errorf("invalid book ID: %w", err) - } - - _, err = r.collection.DeleteOne(ctx, bson.M{"_id": objectID}) + _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookNotFound diff --git a/internal/server/storage/mongo/bookshelf.go b/internal/server/storage/mongo/bookshelf.go index a34a3f7..a08af1f 100644 --- a/internal/server/storage/mongo/bookshelf.go +++ b/internal/server/storage/mongo/bookshelf.go @@ -57,13 +57,9 @@ func (r *BookshelfRepo) Create(ctx context.Context, bookshelf *models.Bookshelf) // GetByID retrieves a bookshelf from the database by its ID. func (r *BookshelfRepo) GetByID(ctx context.Context, id string) (*models.Bookshelf, error) { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return nil, fmt.Errorf("invalid bookshelf ID: %w", err) - } - var bookshelf models.Bookshelf - err = r.collection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&bookshelf) + err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&bookshelf) + fmt.Printf("%+v\n", bookshelf) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return nil, ErrBookshelfNotFound @@ -76,16 +72,11 @@ func (r *BookshelfRepo) GetByID(ctx context.Context, id string) (*models.Bookshe // GetByUser retrieves bookshelf associated with a specific user ID. func (r *BookshelfRepo) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { - objectUserID, err := primitive.ObjectIDFromHex(userID) - if err != nil { - return nil, fmt.Errorf("invalid user ID: %w", err) - } - findOptions := options.Find() findOptions.SetSkip((page - 1) * limit) findOptions.SetLimit(limit) - cursor, err := r.collection.Find(ctx, bson.M{"user_id": objectUserID}, findOptions) + cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID}, findOptions) if err != nil { r.log.Error("failed to get bookshelf by user ID", slog.Any("error", err)) return nil, fmt.Errorf("failed to get bookshelf by user ID: %w", err) @@ -102,12 +93,7 @@ func (r *BookshelfRepo) GetByUser(ctx context.Context, userID string, page int64 // CountByUser returns the number of bookshelf owned by a user. func (r *BookshelfRepo) CountByUser(ctx context.Context, userID string) (int, error) { - objectUserID, err := primitive.ObjectIDFromHex(userID) - if err != nil { - return 0, fmt.Errorf("invalid user ID: %w", err) - } - - count, err := r.collection.CountDocuments(ctx, bson.M{"user_id": objectUserID}) + count, err := r.collection.CountDocuments(ctx, bson.M{"user_id": userID}) if err != nil { r.log.Error("failed to count bookshelf by user ID", slog.Any("error", err)) return 0, fmt.Errorf("failed to count bookshelf by user ID: %w", err) @@ -118,12 +104,7 @@ func (r *BookshelfRepo) CountByUser(ctx context.Context, userID string) (int, er // ExistsByNameAndUser checks if a bookshelf with the given name already exists for a user. func (r *BookshelfRepo) ExistsByNameAndUser(ctx context.Context, name, userID string) (bool, error) { - objectUserID, err := primitive.ObjectIDFromHex(userID) - if err != nil { - return false, fmt.Errorf("invalid user ID: %w", err) - } - - count, err := r.collection.CountDocuments(ctx, bson.M{"name": name, "user_id": objectUserID}) + count, err := r.collection.CountDocuments(ctx, bson.M{"name": name, "user_id": userID}) if err != nil { r.log.Error("failed to check bookshelf existence by name and user", slog.Any("error", err)) return false, fmt.Errorf("failed to check bookshelf existence by name and user: %w", err) @@ -134,13 +115,8 @@ func (r *BookshelfRepo) ExistsByNameAndUser(ctx context.Context, name, userID st // Update updates a bookshelf in the database. func (r *BookshelfRepo) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return fmt.Errorf("invalid bookshelf ID: %w", err) - } - update.UpdatedAt = time.Now() - _, err = r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, bson.M{"$set": update}) + _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookshelfNotFound @@ -152,12 +128,7 @@ func (r *BookshelfRepo) Update(ctx context.Context, id string, update *models.Bo // Delete removes a bookshelf from the database. func (r *BookshelfRepo) Delete(ctx context.Context, id string) error { - objectID, err := primitive.ObjectIDFromHex(id) - if err != nil { - return fmt.Errorf("invalid bookshelf ID: %w", err) - } - - _, err = r.collection.DeleteOne(ctx, bson.M{"_id": objectID}) + _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return ErrBookshelfNotFound diff --git a/internal/server/storage/mongo/user.go b/internal/server/storage/mongo/user.go deleted file mode 100644 index d717812..0000000 --- a/internal/server/storage/mongo/user.go +++ /dev/null @@ -1,101 +0,0 @@ -package mongo - -import ( - "context" - "errors" - "fmt" - "github.com/getz-devs/librakeeper-server/internal/server/models" - "github.com/getz-devs/librakeeper-server/internal/server/repository" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "log/slog" - "time" -) - -// ErrUserNotFound occurs when a user is not found in the database. -var ErrUserNotFound = errors.New("user not found") - -// ErrUserAlreadyExists occurs when trying to create a user with an ID that already exists. -var ErrUserAlreadyExists = errors.New("user already exists") - -// UserRepo implements the repository.UserRepo interface for MongoDB. -type UserRepo struct { - collection *mongo.Collection - log *slog.Logger -} - -// NewUserRepo creates a new UserRepo instance. -func NewUserRepo(db *mongo.Database, log *slog.Logger) repository.UserRepo { - return &UserRepo{ - collection: db.Collection("user"), - log: log, - } -} - -// Create inserts a new user into the database. -func (r *UserRepo) Create(ctx context.Context, user *models.User) error { - user.CreatedAt = time.Now() - user.UpdatedAt = time.Now() - - _, err := r.collection.InsertOne(ctx, user) - if err != nil { - // Check for duplicate key error - var writeErr mongo.WriteException - if errors.As(err, &writeErr) && writeErr.WriteErrors[0].Code == 11000 { - return ErrUserAlreadyExists - } - - r.log.Error("failed to create user", slog.Any("error", err)) - return fmt.Errorf("failed to create user: %w", err) - } - - return nil -} - -// GetByID retrieves a user from the database by their ID. -func (r *UserRepo) GetByID(ctx context.Context, id string) (*models.User, error) { - var user models.User - err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&user) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return nil, ErrUserNotFound - } - return nil, fmt.Errorf("failed to get user: %w", err) - } - return &user, nil -} - -// ExistsByID checks if a user with the given ID already exists. -func (r *UserRepo) ExistsByID(ctx context.Context, id string) (bool, error) { - count, err := r.collection.CountDocuments(ctx, bson.M{"_id": id}) - if err != nil { - r.log.Error("failed to check user existence by ID", slog.Any("error", err)) - return false, fmt.Errorf("failed to check user existence by ID: %w", err) - } - return count > 0, nil -} - -// Update updates a user in the database. -func (r *UserRepo) Update(ctx context.Context, id string, update *models.UserUpdate) error { - update.UpdatedAt = time.Now() - _, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": update}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return ErrUserNotFound - } - return fmt.Errorf("failed to update user: %w", err) - } - return nil -} - -// Delete removes a user from the database. -func (r *UserRepo) Delete(ctx context.Context, id string) error { - _, err := r.collection.DeleteOne(ctx, bson.M{"_id": id}) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return ErrUserNotFound - } - return fmt.Errorf("failed to delete user: %w", err) - } - return nil -} From a428aaf9e508b902deaa8241f9cc6829f6a81c0a Mon Sep 17 00:00:00 2001 From: Den Date: Mon, 15 Jul 2024 21:15:36 +0300 Subject: [PATCH 40/56] add search interfaces & minor stuff --- internal/server/handlers/search.go | 44 +++++++++++++++++++++ internal/server/repository/search.go | 10 +++++ internal/server/routes/routes.go | 9 ++++- internal/server/server.go | 13 +++--- internal/server/services/search/search.go | 32 +++++++++++++++ internal/server/services/search/searcher.go | 31 +++++++++++++++ internal/server/storage/mongo/book.go | 4 +- 7 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 internal/server/handlers/search.go create mode 100644 internal/server/repository/search.go create mode 100644 internal/server/services/search/search.go create mode 100644 internal/server/services/search/searcher.go diff --git a/internal/server/handlers/search.go b/internal/server/handlers/search.go new file mode 100644 index 0000000..a59ef56 --- /dev/null +++ b/internal/server/handlers/search.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "errors" + "github.com/getz-devs/librakeeper-server/internal/server/services/search" + "github.com/gin-gonic/gin" + "log/slog" + "net/http" +) + +// SearchHandlers handles HTTP requests related to search. +type SearchHandlers struct { + service *search.SearchService + log *slog.Logger +} + +func NewSearchHandlers(service *search.SearchService, log *slog.Logger) *SearchHandlers { + return &SearchHandlers{ + service: service, + log: log, + } +} + +func (s *SearchHandlers) Simple(c *gin.Context) { + isbn := c.Param("isbn") + ctx := c.Request.Context() + resp, err := s.service.Simple(ctx, isbn) + if err != nil { + if errors.Is(err, search.ErrISBNNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + s.log.Error("failed to get bookshelf", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelf"}) + return + } + + c.JSON(http.StatusOK, resp) + +} + +func (s *SearchHandlers) Advanced(c *gin.Context) { + +} diff --git a/internal/server/repository/search.go b/internal/server/repository/search.go new file mode 100644 index 0000000..4858b1b --- /dev/null +++ b/internal/server/repository/search.go @@ -0,0 +1,10 @@ +package repository + +import ( + "context" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" +) + +type SearchRepo interface { + SearchByISBN(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) +} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index a9448b6..02c8e32 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -10,6 +10,7 @@ import ( type Handlers struct { Bookshelves *handlers.BookshelfHandlers Books *handlers.BookHandlers + Search *handlers.SearchHandlers } // SetupRoutes sets up the API routes for the server. @@ -32,10 +33,16 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { booksGroup := api.Group("/books") { booksGroup.GET("/", middlewares.AuthMiddleware(), h.Books.GetByUser) - booksGroup.GET("/:id", h.Books.GetByID) + booksGroup.GET("/:id", middlewares.AuthMiddleware(), h.Books.GetByID) booksGroup.GET("/bookshelf/:id", middlewares.AuthMiddleware(), h.Books.GetByBookshelfID) booksGroup.POST("/", middlewares.AuthMiddleware(), h.Books.Create) booksGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Books.Update) booksGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Books.Delete) } + + searchGroup := api.Group("/search") + { + searchGroup.GET("/simple", middlewares.AuthMiddleware(), h.Search.Simple) + searchGroup.GET("/advanced", middlewares.AuthMiddleware(), h.Search.Advanced) + } } diff --git a/internal/server/server.go b/internal/server/server.go index c1834a3..5b21d83 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,8 +10,8 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/routes" "github.com/getz-devs/librakeeper-server/internal/server/services/book" "github.com/getz-devs/librakeeper-server/internal/server/services/bookshelf" + "github.com/getz-devs/librakeeper-server/internal/server/services/search" "github.com/getz-devs/librakeeper-server/internal/server/services/storage" - "github.com/getz-devs/librakeeper-server/internal/server/services/user" "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -53,18 +53,19 @@ func (s *Server) Initialize() error { return fmt.Errorf("failed to initialize Database: %w", err) } - userRepo := mongo.NewUserRepo(db, s.log) - bookRepo := mongo.NewBookRepo(db, s.log) + bookRepo := mongo.NewBookRepo(db, s.log, "user_books") + allBooksRepo := mongo.NewBookRepo(db, s.log, "all_books") bookshelfRepo := mongo.NewBookshelfRepo(db, s.log) + searcherClient := search.NewSearcherClient(s.log) // TODO - userService := user.NewUserService(userRepo, s.log) bookService := book.NewBookService(bookRepo, bookshelfRepo, s.log) bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) + searchService := search.NewSearchService(searcherClient, allBooksRepo, s.log) h := &routes.Handlers{ - Users: handlers.NewUserHandlers(userService, s.log), - Bookshelves: handlers.NewBookshelfHandlers(bookshelfService, s.log), Books: handlers.NewBookHandlers(bookService, s.log), + Bookshelves: handlers.NewBookshelfHandlers(bookshelfService, s.log), + Search: handlers.NewSearchHandlers(searchService, s.log), } // Configure CORS diff --git a/internal/server/services/search/search.go b/internal/server/services/search/search.go new file mode 100644 index 0000000..820e1c4 --- /dev/null +++ b/internal/server/services/search/search.go @@ -0,0 +1,32 @@ +package search + +import ( + "context" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "log/slog" +) + +// SearchService handles business logic for search. +type SearchService struct { + searcher repository.SearchRepo + allBooksRepo repository.BookRepo + log *slog.Logger +} + +func (s *SearchService) Simple(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { + // allBooksRepo.Search(isbn) + return nil, nil +} + +func (s *SearchService) Advanced(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { + // searcher.Search(isbn) + return nil, nil +} + +func NewSearchService(client repository.SearchRepo, repo repository.BookRepo, log *slog.Logger) *SearchService { + return &SearchService{ + searcher: client, + allBooksRepo: repo, + } +} diff --git a/internal/server/services/search/searcher.go b/internal/server/services/search/searcher.go new file mode 100644 index 0000000..751aa80 --- /dev/null +++ b/internal/server/services/search/searcher.go @@ -0,0 +1,31 @@ +package search + +import ( + "context" + "errors" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/server/repository" + "log/slog" +) + +var ( + ErrISBNNotFound = errors.New("ISBN not found") + ErrISBNRequired = errors.New("ISBN is required") +) + +type SearcherClient struct { + // grpc + log *slog.Logger +} + +func (s SearcherClient) SearchByISBN(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + +func NewSearcherClient(log *slog.Logger) repository.SearchRepo { + return &SearcherClient{ + log: log, + } +} diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index 3843eb2..5028c82 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -27,9 +27,9 @@ type BookRepo struct { } // NewBookRepo creates a new BookRepo instance. -func NewBookRepo(db *mongo.Database, log *slog.Logger) repository.BookRepo { +func NewBookRepo(db *mongo.Database, log *slog.Logger, collectionName string) repository.BookRepo { return &BookRepo{ - collection: db.Collection("book"), // Note: Collection name corrected to "book" + collection: db.Collection(collectionName), log: log, } } From e6e9c46fef3f93e3ef5944a4b000e30e4c66dbfc Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 02:01:17 +0300 Subject: [PATCH 41/56] connect to grpc and call search methods --- internal/server/config/config.go | 5 +++ internal/server/handlers/search.go | 16 ++++++++- internal/server/server.go | 12 ++++++- internal/server/services/search/search.go | 23 +++++++++++-- internal/server/services/search/searcher.go | 36 ++++++++++++++++----- 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/internal/server/config/config.go b/internal/server/config/config.go index 3ac5047..b4da113 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -9,6 +9,7 @@ import ( ) // Config represents the application's configuration. + type Config struct { Env string `yaml:"env" env-default:"local"` @@ -25,6 +26,10 @@ type Config struct { Auth struct { ConfigPath string `yaml:"config_path" env-required:"true"` } `yaml:"auth"` + + GRPC struct { + Addr string `yaml:"addr" env-default:"localhost:44044"` + } `yaml:"grpc"` } // MustLoad loads the configuration from the specified path and environment variables. diff --git a/internal/server/handlers/search.go b/internal/server/handlers/search.go index a59ef56..1749839 100644 --- a/internal/server/handlers/search.go +++ b/internal/server/handlers/search.go @@ -40,5 +40,19 @@ func (s *SearchHandlers) Simple(c *gin.Context) { } func (s *SearchHandlers) Advanced(c *gin.Context) { - + const op = "handlers.SearchHandlers.Advanced" + log := s.log.With(slog.String("op", op)) + isbn := c.Param("isbn") + ctx := c.Request.Context() + resp, err := s.service.Advanced(ctx, isbn) + if err != nil { + if errors.Is(err, search.ErrISBNNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + log.Error("failed to search", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"}) + return + } + c.JSON(http.StatusOK, resp) } diff --git a/internal/server/server.go b/internal/server/server.go index 5b21d83..b7b2287 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -15,6 +15,8 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "log/slog" "net/http" "os" @@ -53,10 +55,18 @@ func (s *Server) Initialize() error { return fmt.Errorf("failed to initialize Database: %w", err) } + conn, err := grpc.NewClient( + s.config.GRPC.Addr, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + panic(err) + } + bookRepo := mongo.NewBookRepo(db, s.log, "user_books") allBooksRepo := mongo.NewBookRepo(db, s.log, "all_books") bookshelfRepo := mongo.NewBookshelfRepo(db, s.log) - searcherClient := search.NewSearcherClient(s.log) // TODO + searcherClient := search.NewSearcherClient(conn, s.log) bookService := book.NewBookService(bookRepo, bookshelfRepo, s.log) bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) diff --git a/internal/server/services/search/search.go b/internal/server/services/search/search.go index 820e1c4..beb2ddf 100644 --- a/internal/server/services/search/search.go +++ b/internal/server/services/search/search.go @@ -7,26 +7,43 @@ import ( "log/slog" ) -// SearchService handles business logic for search. type SearchService struct { searcher repository.SearchRepo allBooksRepo repository.BookRepo log *slog.Logger } +// Simple выполняет простой поиск по ISBN в локальной базе данных. func (s *SearchService) Simple(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { // allBooksRepo.Search(isbn) return nil, nil } +// Advanced выполняет расширенный поиск по ISBN с использованием gRPC. func (s *SearchService) Advanced(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { - // searcher.Search(isbn) - return nil, nil + const op = "search.SearchService.Advanced" + log := s.log.With(slog.String("op", op), slog.String("isbn", isbn)) + + if isbn == "" { + return nil, ErrISBNRequired + } + + response, err := s.searcher.SearchByISBN(ctx, isbn) + if err != nil { + log.Error("failed to search by ISBN", slog.Any("error", err)) + return nil, err + } + + // Здесь вы можете добавить дополнительную обработку ответа от gRPC-сервиса, + // например, обогатить данные из allBooksRepo. + + return response, nil } func NewSearchService(client repository.SearchRepo, repo repository.BookRepo, log *slog.Logger) *SearchService { return &SearchService{ searcher: client, allBooksRepo: repo, + log: log, } } diff --git a/internal/server/services/search/searcher.go b/internal/server/services/search/searcher.go index 751aa80..2a03e1a 100644 --- a/internal/server/services/search/searcher.go +++ b/internal/server/services/search/searcher.go @@ -5,7 +5,9 @@ import ( "errors" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" "github.com/getz-devs/librakeeper-server/internal/server/repository" + "google.golang.org/grpc" "log/slog" + "time" ) var ( @@ -14,18 +16,36 @@ var ( ) type SearcherClient struct { - // grpc - log *slog.Logger + client searcherv1.SearcherClient // Используем gRPC клиент напрямую + log *slog.Logger } -func (s SearcherClient) SearchByISBN(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { - //TODO implement me - panic("implement me") - return nil, nil +func (s *SearcherClient) SearchByISBN(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { + const op = "search.SearcherClient.SearchByISBN" + log := s.log.With(slog.String("op", op), slog.String("isbn", isbn)) + + if isbn == "" { + return nil, ErrISBNRequired + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + // Используем client напрямую, без создания нового + resp, err := s.client.SearchByISBN(ctx, &searcherv1.SearchByISBNRequest{Isbn: isbn}) + if err != nil { + log.Error("failed to search by isbn", slog.Any("error", err)) + return nil, err + } + + return resp, nil } -func NewSearcherClient(log *slog.Logger) repository.SearchRepo { +func NewSearcherClient(conn *grpc.ClientConn, log *slog.Logger) repository.SearchRepo { // Передаем конфигурацию + client := searcherv1.NewSearcherClient(conn) // Создаем gRPC клиент + return &SearcherClient{ - log: log, + client: client, // Сохраняем клиент в структуре + log: log, } } From d13dd88cba571854b20b4bcb4e9c0b1952808794 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 04:36:53 +0300 Subject: [PATCH 42/56] configured to work in docker stack & fix isbn query --- config/server/docker-local.yaml | 7 +++++-- internal/server/handlers/search.go | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/config/server/docker-local.yaml b/config/server/docker-local.yaml index 96b09d3..3d3daf2 100644 --- a/config/server/docker-local.yaml +++ b/config/server/docker-local.yaml @@ -3,8 +3,8 @@ env: "local" server: port: 8080 allowed_origins: - - https://* - - http://* + - https://libra.potat.dev + - http://localhost:3000 database: uri: mongodb://mongo:27017 @@ -12,3 +12,6 @@ database: auth: config_path: /config/secret.json + +grpc: + addr: searcher:8081 \ No newline at end of file diff --git a/internal/server/handlers/search.go b/internal/server/handlers/search.go index 1749839..987980e 100644 --- a/internal/server/handlers/search.go +++ b/internal/server/handlers/search.go @@ -22,7 +22,7 @@ func NewSearchHandlers(service *search.SearchService, log *slog.Logger) *SearchH } func (s *SearchHandlers) Simple(c *gin.Context) { - isbn := c.Param("isbn") + isbn := c.Query("isbn") ctx := c.Request.Context() resp, err := s.service.Simple(ctx, isbn) if err != nil { @@ -42,7 +42,7 @@ func (s *SearchHandlers) Simple(c *gin.Context) { func (s *SearchHandlers) Advanced(c *gin.Context) { const op = "handlers.SearchHandlers.Advanced" log := s.log.With(slog.String("op", op)) - isbn := c.Param("isbn") + isbn := c.Query("isbn") ctx := c.Request.Context() resp, err := s.service.Advanced(ctx, isbn) if err != nil { From 8ae164f733e503bec17f82a6fd678ba13017ec35 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 05:13:49 +0300 Subject: [PATCH 43/56] add to all books implemented --- internal/server/handlers/book.go | 10 ++++- internal/server/handlers/search.go | 8 ++-- internal/server/server.go | 2 +- internal/server/services/book/book.go | 49 ++++++++++++++++++++++- internal/server/services/search/search.go | 33 ++++++++++++++- 5 files changed, 92 insertions(+), 10 deletions(-) diff --git a/internal/server/handlers/book.go b/internal/server/handlers/book.go index 1d69fda..034bd83 100644 --- a/internal/server/handlers/book.go +++ b/internal/server/handlers/book.go @@ -42,7 +42,15 @@ func (h *BookHandlers) Create(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) - if err := h.service.Create(ctx, &b); err != nil { + // Получаем флаг addToAll из параметров запроса + addToAllStr := c.Query("addToAll") + addToAll, _ := strconv.ParseBool(addToAllStr) // По умолчанию addToAll == false + + // Вызываем сервис с addToAll + if err := h.service.Create(ctx, &b, addToAll); err != nil { + if errors.Is(err, book.ErrCantAddToAllBooks) { + c.JSON(http.StatusPartialContent, gin.H{"error": err.Error()}) + } h.log.Error("failed to create book", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) return diff --git a/internal/server/handlers/search.go b/internal/server/handlers/search.go index 987980e..5758017 100644 --- a/internal/server/handlers/search.go +++ b/internal/server/handlers/search.go @@ -22,6 +22,8 @@ func NewSearchHandlers(service *search.SearchService, log *slog.Logger) *SearchH } func (s *SearchHandlers) Simple(c *gin.Context) { + const op = "handlers.SearchHandlers.Simple" + log := s.log.With(slog.String("op", op)) isbn := c.Query("isbn") ctx := c.Request.Context() resp, err := s.service.Simple(ctx, isbn) @@ -30,13 +32,11 @@ func (s *SearchHandlers) Simple(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } - s.log.Error("failed to get bookshelf", slog.Any("error", err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bookshelf"}) + log.Error("failed to search", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"}) return } - c.JSON(http.StatusOK, resp) - } func (s *SearchHandlers) Advanced(c *gin.Context) { diff --git a/internal/server/server.go b/internal/server/server.go index b7b2287..6009639 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -68,7 +68,7 @@ func (s *Server) Initialize() error { bookshelfRepo := mongo.NewBookshelfRepo(db, s.log) searcherClient := search.NewSearcherClient(conn, s.log) - bookService := book.NewBookService(bookRepo, bookshelfRepo, s.log) + bookService := book.NewBookService(bookRepo, allBooksRepo, bookshelfRepo, s.log) bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) searchService := search.NewSearchService(searcherClient, allBooksRepo, s.log) diff --git a/internal/server/services/book/book.go b/internal/server/services/book/book.go index b15c61c..fb08ec7 100644 --- a/internal/server/services/book/book.go +++ b/internal/server/services/book/book.go @@ -8,6 +8,7 @@ import ( "github.com/getz-devs/librakeeper-server/internal/server/repository" "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" + "time" ) // Custom Error Types: @@ -19,20 +20,23 @@ var ( ErrTitleAndAuthorRequired = errors.New("book title and author are required") ErrBookshelfLimitReached = errors.New("bookshelf has reached the book limit") ErrBookAlreadyExists = errors.New("book with this ISBN already exists in this bookshelf") + ErrCantAddToAllBooks = errors.New("error adding book to all books") ) // BookService defines the interface for book service operations. type BookService struct { repo repository.BookRepo + allBooksRepo repository.BookRepo bookshelfRepo repository.BookshelfRepo log *slog.Logger bookLimit int } // NewBookService creates a new BookService instance. -func NewBookService(repo repository.BookRepo, bookshelfRepo repository.BookshelfRepo, log *slog.Logger) *BookService { +func NewBookService(repo repository.BookRepo, allBooksRepo repository.BookRepo, bookshelfRepo repository.BookshelfRepo, log *slog.Logger) *BookService { return &BookService{ repo: repo, + allBooksRepo: allBooksRepo, bookshelfRepo: bookshelfRepo, log: log, bookLimit: 1000, // TODO: Read from config @@ -40,7 +44,7 @@ func NewBookService(repo repository.BookRepo, bookshelfRepo repository.Bookshelf } // Create creates a new book. -func (s *BookService) Create(ctx context.Context, book *models.Book) error { +func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bool) error { // Rule 2: Book Title & Author Presence if book.Title == "" || book.Author == "" { return ErrTitleAndAuthorRequired @@ -86,6 +90,47 @@ func (s *BookService) Create(ctx context.Context, book *models.Book) error { return fmt.Errorf("failed to create book: %w", err) } + // Добавляем книгу в allBooksRepo, если addToAll == true + if addToAll { + // Перед добавлением в allBooksRepo, убедитесь, что книга еще не существует + existingBook, err := s.allBooksRepo.GetByID(ctx, book.ISBN) + if err != nil && !errors.Is(err, mongo.ErrBookNotFound) { + s.log.Error("failed to check for existing book in allBooksRepo", slog.Any("error", err)) + return fmt.Errorf("failed to check for existing book in allBooksRepo: %w", err) + } + + if errors.Is(err, mongo.ErrBookNotFound) { + // Книги нет в allBooksRepo, можно добавить + allBook := &models.Book{ + Title: book.Title, + Author: book.Author, + ISBN: book.ISBN, + Description: book.Description, + CoverImage: book.CoverImage, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.allBooksRepo.Create(ctx, allBook); err != nil { + s.log.Error("failed to create book in allBooksRepo", slog.Any("error", err)) + return ErrCantAddToAllBooks + } + } else { + // Книга уже есть в allBooksRepo, обновляем информацию + update := &models.BookUpdate{ + Title: &book.Title, + Author: &book.Author, + Description: &book.Description, + CoverImage: &book.CoverImage, + UpdatedAt: time.Now(), + } + if err := s.allBooksRepo.Update(ctx, existingBook.ID, update); err != nil { + s.log.Error("failed to update book in allBooksRepo", slog.Any("error", err)) + return ErrCantAddToAllBooks + } + } + } + return nil } diff --git a/internal/server/services/search/search.go b/internal/server/services/search/search.go index beb2ddf..e0208e8 100644 --- a/internal/server/services/search/search.go +++ b/internal/server/services/search/search.go @@ -2,8 +2,10 @@ package search import ( "context" + "errors" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" "github.com/getz-devs/librakeeper-server/internal/server/repository" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" ) @@ -15,8 +17,35 @@ type SearchService struct { // Simple выполняет простой поиск по ISBN в локальной базе данных. func (s *SearchService) Simple(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { - // allBooksRepo.Search(isbn) - return nil, nil + const op = "search.SearchService.Simple" + log := s.log.With(slog.String("op", op), slog.String("isbn", isbn)) + + if isbn == "" { + return nil, ErrISBNRequired + } + + book, err := s.allBooksRepo.GetByID(ctx, isbn) + if err != nil { + if errors.Is(err, mongo.ErrBookNotFound) { + return nil, ErrISBNNotFound + } + log.Error("failed to get book from allBooksRepo", slog.Any("error", err)) + return nil, err + } + + // Преобразуем models.Book в searcherv1.Book + protoBook := &searcherv1.Book{ + Title: book.Title, + Author: book.Author, + Publishing: book.Description, + ImgUrl: book.CoverImage, + ShopName: "", // TODO: change book to proto book + } + + return &searcherv1.SearchByISBNResponse{ + Status: searcherv1.SearchByISBNResponse_SUCCESS, + Books: []*searcherv1.Book{protoBook}, + }, nil } // Advanced выполняет расширенный поиск по ISBN с использованием gRPC. From 8b3db767550026e81ce9c646b4185e66ff677868 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 05:42:35 +0300 Subject: [PATCH 44/56] rewrite search result to server model --- internal/server/handlers/search.go | 10 ++++-- internal/server/models/book.go | 28 +++++++++------- internal/server/models/search.go | 8 +++++ internal/server/services/search/search.go | 40 +++++++++++++---------- 4 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 internal/server/models/search.go diff --git a/internal/server/handlers/search.go b/internal/server/handlers/search.go index 5758017..1251a2d 100644 --- a/internal/server/handlers/search.go +++ b/internal/server/handlers/search.go @@ -26,6 +26,8 @@ func (s *SearchHandlers) Simple(c *gin.Context) { log := s.log.With(slog.String("op", op)) isbn := c.Query("isbn") ctx := c.Request.Context() + + // Вызываем метод Simple, который теперь возвращает models.SearchResponse resp, err := s.service.Simple(ctx, isbn) if err != nil { if errors.Is(err, search.ErrISBNNotFound) { @@ -36,7 +38,8 @@ func (s *SearchHandlers) Simple(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"}) return } - c.JSON(http.StatusOK, resp) + + c.JSON(http.StatusOK, resp) // Возвращаем models.SearchResponse в JSON } func (s *SearchHandlers) Advanced(c *gin.Context) { @@ -44,6 +47,8 @@ func (s *SearchHandlers) Advanced(c *gin.Context) { log := s.log.With(slog.String("op", op)) isbn := c.Query("isbn") ctx := c.Request.Context() + + // Вызываем метод Advanced, который теперь возвращает models.SearchResponse resp, err := s.service.Advanced(ctx, isbn) if err != nil { if errors.Is(err, search.ErrISBNNotFound) { @@ -54,5 +59,6 @@ func (s *SearchHandlers) Advanced(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search"}) return } - c.JSON(http.StatusOK, resp) + + c.JSON(http.StatusOK, resp) // Возвращаем models.SearchResponse в JSON } diff --git a/internal/server/models/book.go b/internal/server/models/book.go index 6a4327b..304ab8e 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -6,24 +6,30 @@ import ( // Book represents a book in the library. type Book struct { - ID string `bson:"_id,omitempty" json:"id"` - UserID string `bson:"user_id" json:"user_id"` - BookshelfID string `bson:"bookshelf_id" json:"bookshelf_id"` - Title string `bson:"title" json:"title"` - Author string `bson:"author" json:"author"` - ISBN string `bson:"isbn" json:"isbn"` - Description string `bson:"description" json:"description"` - CoverImage string `bson:"cover_image" json:"cover_image"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + ID string `bson:"_id,omitempty" json:"id"` + UserID string `bson:"user_id" json:"user_id"` + BookshelfID string `bson:"bookshelf_id" json:"bookshelf_id"` + + ISBN string `bson:"isbn" json:"isbn"` + Title string `bson:"title" json:"title"` + Author string `bson:"author" json:"author"` + Publishing string `bson:"publishing" json:"publishing"` + Description string `bson:"description" json:"description"` + CoverImage string `bson:"cover_image" json:"cover_image"` + ShopName string `bson:"shop_name" json:"shop_name"` + + CreatedAt time.Time `bson:"created_at" json:"created_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } // BookUpdate represents fields that can be updated in a Book. type BookUpdate struct { + ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` Title *string `bson:"title,omitempty" json:"title,omitempty"` // Optional fields for update Author *string `bson:"author,omitempty" json:"author,omitempty"` - ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` + Publishing *string `bson:"publishing" json:"publishing"` Description *string `bson:"description,omitempty" json:"description,omitempty"` CoverImage *string `bson:"cover_image,omitempty" json:"cover_image,omitempty"` + ShopName *string `bson:"shop_name" json:"shop_name"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } diff --git a/internal/server/models/search.go b/internal/server/models/search.go new file mode 100644 index 0000000..f53dbfb --- /dev/null +++ b/internal/server/models/search.go @@ -0,0 +1,8 @@ +package models + +import searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + +type SearchResponse struct { + Status searcherv1.SearchByISBNResponse_Status `json:"status"` + Books []*Book `json:"books"` +} diff --git a/internal/server/services/search/search.go b/internal/server/services/search/search.go index e0208e8..4298f98 100644 --- a/internal/server/services/search/search.go +++ b/internal/server/services/search/search.go @@ -4,6 +4,7 @@ import ( "context" "errors" searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/repository" "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" @@ -16,7 +17,7 @@ type SearchService struct { } // Simple выполняет простой поиск по ISBN в локальной базе данных. -func (s *SearchService) Simple(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { +func (s *SearchService) Simple(ctx context.Context, isbn string) (*models.SearchResponse, error) { const op = "search.SearchService.Simple" log := s.log.With(slog.String("op", op), slog.String("isbn", isbn)) @@ -33,23 +34,14 @@ func (s *SearchService) Simple(ctx context.Context, isbn string) (*searcherv1.Se return nil, err } - // Преобразуем models.Book в searcherv1.Book - protoBook := &searcherv1.Book{ - Title: book.Title, - Author: book.Author, - Publishing: book.Description, - ImgUrl: book.CoverImage, - ShopName: "", // TODO: change book to proto book - } - - return &searcherv1.SearchByISBNResponse{ + return &models.SearchResponse{ Status: searcherv1.SearchByISBNResponse_SUCCESS, - Books: []*searcherv1.Book{protoBook}, + Books: []*models.Book{book}, // Используем найденный объект book напрямую }, nil } // Advanced выполняет расширенный поиск по ISBN с использованием gRPC. -func (s *SearchService) Advanced(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { +func (s *SearchService) Advanced(ctx context.Context, isbn string) (*models.SearchResponse, error) { // Измените тип возвращаемого значения const op = "search.SearchService.Advanced" log := s.log.With(slog.String("op", op), slog.String("isbn", isbn)) @@ -57,16 +49,30 @@ func (s *SearchService) Advanced(ctx context.Context, isbn string) (*searcherv1. return nil, ErrISBNRequired } - response, err := s.searcher.SearchByISBN(ctx, isbn) + grpcResponse, err := s.searcher.SearchByISBN(ctx, isbn) // Получаем ответ от gRPC сервиса if err != nil { log.Error("failed to search by ISBN", slog.Any("error", err)) return nil, err } - // Здесь вы можете добавить дополнительную обработку ответа от gRPC-сервиса, - // например, обогатить данные из allBooksRepo. + // Преобразуем gRPC ответ в models.SearchResponse + var books []*models.Book + for _, protoBook := range grpcResponse.Books { + book := &models.Book{ + Title: protoBook.Title, + Author: protoBook.Author, + Publishing: protoBook.Publishing, + CoverImage: protoBook.ImgUrl, + ShopName: protoBook.ShopName, + // ... другие поля, если необходимо + } + books = append(books, book) + } - return response, nil + return &models.SearchResponse{ + Status: grpcResponse.Status, + Books: books, + }, nil } func NewSearchService(client repository.SearchRepo, repo repository.BookRepo, log *slog.Logger) *SearchService { From 59cc823907e5152436ceb9bdf5f1af7db2f90313 Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 05:58:48 +0300 Subject: [PATCH 45/56] BookshelfID no more required & add GetByISBN --- internal/server/repository/book.go | 1 + internal/server/services/book/book.go | 73 +++++++++++++-------------- internal/server/storage/mongo/book.go | 13 +++++ 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/internal/server/repository/book.go b/internal/server/repository/book.go index 57c28c2..75a3ba1 100644 --- a/internal/server/repository/book.go +++ b/internal/server/repository/book.go @@ -9,6 +9,7 @@ import ( type BookRepo interface { Create(ctx context.Context, book *models.Book) error GetByID(ctx context.Context, id string) (*models.Book, error) + GetByISBN(ctx context.Context, isbn string) (*models.Book, error) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) diff --git a/internal/server/services/book/book.go b/internal/server/services/book/book.go index fb08ec7..51b8b30 100644 --- a/internal/server/services/book/book.go +++ b/internal/server/services/book/book.go @@ -56,34 +56,38 @@ func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bo return ErrUserNotFoundInContext } - bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) - if err != nil { - if errors.Is(err, mongo.ErrBookshelfNotFound) { - return ErrBookshelfNotFound + if book.BookshelfID != "" { + // if bookshelf specified, check bookshelf + + bookshelf, err := s.bookshelfRepo.GetByID(ctx, book.BookshelfID) + if err != nil { + if errors.Is(err, mongo.ErrBookshelfNotFound) { + return ErrBookshelfNotFound + } + return fmt.Errorf("failed to get bookshelf: %w", err) } - return fmt.Errorf("failed to get bookshelf: %w", err) - } - if bookshelf.UserID != userID { - return ErrNotAuthorized - } + if bookshelf.UserID != userID { + return ErrNotAuthorized + } - // Rule 4: Book Limit per Bookshelf - bookCount, err := s.repo.CountInBookshelf(ctx, book.BookshelfID) - if err != nil { - return fmt.Errorf("failed to get book count for bookshelf: %w", err) - } - if bookCount >= s.bookLimit { - return ErrBookshelfLimitReached - } + // Rule 4: Book Limit per Bookshelf + bookCount, err := s.repo.CountInBookshelf(ctx, book.BookshelfID) + if err != nil { + return fmt.Errorf("failed to get book count for bookshelf: %w", err) + } + if bookCount >= s.bookLimit { + return ErrBookshelfLimitReached + } - // Rule 5: Unique Book within Bookshelf - exists, err := s.repo.ExistsInBookshelf(ctx, book.ISBN, book.BookshelfID) - if err != nil { - return fmt.Errorf("failed to check book existence: %w", err) - } - if exists { - return ErrBookAlreadyExists + // Rule 5: Unique Book within Bookshelf + exists, err := s.repo.ExistsInBookshelf(ctx, book.ISBN, book.BookshelfID) + if err != nil { + return fmt.Errorf("failed to check book existence: %w", err) + } + if exists { + return ErrBookAlreadyExists + } } if err := s.repo.Create(ctx, book); err != nil { @@ -92,8 +96,8 @@ func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bo // Добавляем книгу в allBooksRepo, если addToAll == true if addToAll { - // Перед добавлением в allBooksRepo, убедитесь, что книга еще не существует - existingBook, err := s.allBooksRepo.GetByID(ctx, book.ISBN) + // Проверяем, существует ли книга по ISBN + _, err := s.allBooksRepo.GetByISBN(ctx, book.ISBN) // Используем GetByISBN if err != nil && !errors.Is(err, mongo.ErrBookNotFound) { s.log.Error("failed to check for existing book in allBooksRepo", slog.Any("error", err)) return fmt.Errorf("failed to check for existing book in allBooksRepo: %w", err) @@ -105,30 +109,21 @@ func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bo Title: book.Title, Author: book.Author, ISBN: book.ISBN, + Publishing: book.Publishing, Description: book.Description, CoverImage: book.CoverImage, + ShopName: book.ShopName, CreatedAt: time.Now(), UpdatedAt: time.Now(), + // UserID и BookshelfID не устанавливаем } if err := s.allBooksRepo.Create(ctx, allBook); err != nil { s.log.Error("failed to create book in allBooksRepo", slog.Any("error", err)) return ErrCantAddToAllBooks } - } else { - // Книга уже есть в allBooksRepo, обновляем информацию - update := &models.BookUpdate{ - Title: &book.Title, - Author: &book.Author, - Description: &book.Description, - CoverImage: &book.CoverImage, - UpdatedAt: time.Now(), - } - if err := s.allBooksRepo.Update(ctx, existingBook.ID, update); err != nil { - s.log.Error("failed to update book in allBooksRepo", slog.Any("error", err)) - return ErrCantAddToAllBooks - } } + // Если книга уже существует, ничего не делаем } return nil diff --git a/internal/server/storage/mongo/book.go b/internal/server/storage/mongo/book.go index 5028c82..a0f196c 100644 --- a/internal/server/storage/mongo/book.go +++ b/internal/server/storage/mongo/book.go @@ -68,6 +68,19 @@ func (r *BookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) return &book, nil } +// GetByISBN retrieves a book from the database by its ISBN. +func (r *BookRepo) GetByISBN(ctx context.Context, isbn string) (*models.Book, error) { + var book models.Book + err := r.collection.FindOne(ctx, bson.M{"isbn": isbn}).Decode(&book) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, ErrBookNotFound + } + return nil, fmt.Errorf("failed to get book by ISBN: %w", err) + } + return &book, nil +} + // GetByUserID retrieves book associated with a specific user ID. func (r *BookRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { findOptions := options.Find() From 8252e2952f6d7754a59ac38196c871a3c1b6660d Mon Sep 17 00:00:00 2001 From: Den Date: Tue, 16 Jul 2024 06:44:56 +0300 Subject: [PATCH 46/56] fix mongodb connection, add GetByISBN route --- config/server/docker-local.yaml | 2 +- internal/server/handlers/book.go | 19 +++++++++++++++++++ internal/server/routes/routes.go | 5 +++-- internal/server/services/book/book.go | 12 ++++++++++++ internal/server/services/search/search.go | 4 +++- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/config/server/docker-local.yaml b/config/server/docker-local.yaml index 3d3daf2..ed5853c 100644 --- a/config/server/docker-local.yaml +++ b/config/server/docker-local.yaml @@ -7,7 +7,7 @@ server: - http://localhost:3000 database: - uri: mongodb://mongo:27017 + uri: mongodb://mongodb/ name: docker_server auth: diff --git a/internal/server/handlers/book.go b/internal/server/handlers/book.go index 034bd83..44a1000 100644 --- a/internal/server/handlers/book.go +++ b/internal/server/handlers/book.go @@ -78,6 +78,25 @@ func (h *BookHandlers) GetByID(c *gin.Context) { c.JSON(http.StatusOK, b) } +// GetByISBN retrieves a book by its ISBN. +func (h *BookHandlers) GetByISBN(c *gin.Context) { + isbn := c.Param("isbn") + + ctx := c.Request.Context() + b, err := h.service.GetByISBN(ctx, isbn) + if err != nil { + if errors.Is(err, book.ErrBookNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + h.log.Error("failed to get book by ISBN", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get book by ISBN"}) + return + } + + c.JSON(http.StatusOK, b) +} + // GetByUser retrieves books for a user. func (h *BookHandlers) GetByUser(c *gin.Context) { pageStr := c.DefaultQuery("page", "1") diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 02c8e32..12524ab 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -22,9 +22,9 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { // Bookshelf routes bookshelvesGroup := api.Group("/bookshelves") { + bookshelvesGroup.POST("/add", middlewares.AuthMiddleware(), h.Bookshelves.Create) bookshelvesGroup.GET("/", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) bookshelvesGroup.GET("/:id", middlewares.AuthMiddleware(), h.Bookshelves.GetByID) - bookshelvesGroup.POST("/add", middlewares.AuthMiddleware(), h.Bookshelves.Create) bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Update) bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Delete) } @@ -32,10 +32,11 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { // Book routes booksGroup := api.Group("/books") { + booksGroup.POST("/add", middlewares.AuthMiddleware(), h.Books.Create) booksGroup.GET("/", middlewares.AuthMiddleware(), h.Books.GetByUser) booksGroup.GET("/:id", middlewares.AuthMiddleware(), h.Books.GetByID) + booksGroup.GET("/isbn/:isbn", middlewares.AuthMiddleware(), h.Books.GetByISBN) booksGroup.GET("/bookshelf/:id", middlewares.AuthMiddleware(), h.Books.GetByBookshelfID) - booksGroup.POST("/", middlewares.AuthMiddleware(), h.Books.Create) booksGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Books.Update) booksGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Books.Delete) } diff --git a/internal/server/services/book/book.go b/internal/server/services/book/book.go index 51b8b30..333caf6 100644 --- a/internal/server/services/book/book.go +++ b/internal/server/services/book/book.go @@ -141,6 +141,18 @@ func (s *BookService) GetByID(ctx context.Context, bookID string) (*models.Book, return book, nil } +// GetByISBN retrieves a book by its ISBN. +func (s *BookService) GetByISBN(ctx context.Context, isbn string) (*models.Book, error) { + book, err := s.repo.GetByISBN(ctx, isbn) + if err != nil { + if errors.Is(err, mongo.ErrBookNotFound) { + return nil, ErrBookNotFound + } + return nil, fmt.Errorf("failed to get book by ISBN: %w", err) + } + return book, nil +} + // GetByUserID retrieves a list of book for a specific user. func (s *BookService) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { books, err := s.repo.GetByUserID(ctx, userID, page, limit) diff --git a/internal/server/services/search/search.go b/internal/server/services/search/search.go index 4298f98..61be0e7 100644 --- a/internal/server/services/search/search.go +++ b/internal/server/services/search/search.go @@ -25,7 +25,8 @@ func (s *SearchService) Simple(ctx context.Context, isbn string) (*models.Search return nil, ErrISBNRequired } - book, err := s.allBooksRepo.GetByID(ctx, isbn) + // Используем GetByISBN для поиска по ISBN + book, err := s.allBooksRepo.GetByISBN(ctx, isbn) if err != nil { if errors.Is(err, mongo.ErrBookNotFound) { return nil, ErrISBNNotFound @@ -59,6 +60,7 @@ func (s *SearchService) Advanced(ctx context.Context, isbn string) (*models.Sear var books []*models.Book for _, protoBook := range grpcResponse.Books { book := &models.Book{ + ISBN: isbn, Title: protoBook.Title, Author: protoBook.Author, Publishing: protoBook.Publishing, From c0a45d4f8443d3f735b87226a4a3c24ae323dfb3 Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 17 Jul 2024 09:15:22 +0300 Subject: [PATCH 47/56] rewrite add to all books logic to get book from searcher --- internal/server/handlers/book.go | 34 +++++++--- internal/server/routes/routes.go | 10 +++ internal/server/server.go | 4 +- internal/server/services/book/book.go | 89 +++++++++++++++++---------- 4 files changed, 93 insertions(+), 44 deletions(-) diff --git a/internal/server/handlers/book.go b/internal/server/handlers/book.go index 44a1000..67e3e22 100644 --- a/internal/server/handlers/book.go +++ b/internal/server/handlers/book.go @@ -42,15 +42,8 @@ func (h *BookHandlers) Create(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), "userID", userID) - // Получаем флаг addToAll из параметров запроса - addToAllStr := c.Query("addToAll") - addToAll, _ := strconv.ParseBool(addToAllStr) // По умолчанию addToAll == false - // Вызываем сервис с addToAll - if err := h.service.Create(ctx, &b, addToAll); err != nil { - if errors.Is(err, book.ErrCantAddToAllBooks) { - c.JSON(http.StatusPartialContent, gin.H{"error": err.Error()}) - } + if err := h.service.Create(ctx, &b); err != nil { h.log.Error("failed to create book", slog.Any("error", err)) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) return @@ -229,3 +222,28 @@ func (h *BookHandlers) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Book deleted successfully"}) } + +func (h *BookHandlers) AddAdvanced(c *gin.Context) { + isbn := c.Query("isbn") + indexStr := c.Query("index") + index, err := strconv.Atoi(indexStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"}) + return + } + + // Вызываем сервис с addToAll + err = h.service.AddAdvanced(c.Request.Context(), isbn, index) + if errors.Is(err, book.ErrBookAlreadyExistsInALL) { + c.JSON(http.StatusNotModified, gin.H{"warning": err.Error()}) + return + } + + if err != nil { + h.log.Error("failed to add book to all from advanced search", slog.Any("error", err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create book"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"status": "ok"}) +} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 12524ab..dd92272 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -41,6 +41,16 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { booksGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Books.Delete) } + // Bookshelf routes + bookshelvesGroup := api.Group("/bookshelves") + { + bookshelvesGroup.POST("/add", middlewares.AuthMiddleware(), h.Bookshelves.Create) + bookshelvesGroup.GET("/", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) + bookshelvesGroup.GET("/:id", middlewares.AuthMiddleware(), h.Bookshelves.GetByID) + bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Update) + bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Delete) + } + searchGroup := api.Group("/search") { searchGroup.GET("/simple", middlewares.AuthMiddleware(), h.Search.Simple) diff --git a/internal/server/server.go b/internal/server/server.go index 6009639..c8b1901 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -68,9 +68,9 @@ func (s *Server) Initialize() error { bookshelfRepo := mongo.NewBookshelfRepo(db, s.log) searcherClient := search.NewSearcherClient(conn, s.log) - bookService := book.NewBookService(bookRepo, allBooksRepo, bookshelfRepo, s.log) - bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) searchService := search.NewSearchService(searcherClient, allBooksRepo, s.log) + bookService := book.NewBookService(bookRepo, allBooksRepo, bookshelfRepo, searchService, s.log) + bookshelfService := bookshelf.NewBookshelfService(bookshelfRepo, s.log) h := &routes.Handlers{ Books: handlers.NewBookHandlers(bookService, s.log), diff --git a/internal/server/services/book/book.go b/internal/server/services/book/book.go index 333caf6..83b6a65 100644 --- a/internal/server/services/book/book.go +++ b/internal/server/services/book/book.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/getz-devs/librakeeper-server/internal/server/models" "github.com/getz-devs/librakeeper-server/internal/server/repository" + "github.com/getz-devs/librakeeper-server/internal/server/services/search" "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" "log/slog" "time" @@ -21,6 +22,7 @@ var ( ErrBookshelfLimitReached = errors.New("bookshelf has reached the book limit") ErrBookAlreadyExists = errors.New("book with this ISBN already exists in this bookshelf") ErrCantAddToAllBooks = errors.New("error adding book to all books") + ErrBookAlreadyExistsInALL = errors.New("book already exist in all books") ) // BookService defines the interface for book service operations. @@ -28,23 +30,25 @@ type BookService struct { repo repository.BookRepo allBooksRepo repository.BookRepo bookshelfRepo repository.BookshelfRepo + searcher *search.SearchService log *slog.Logger bookLimit int } // NewBookService creates a new BookService instance. -func NewBookService(repo repository.BookRepo, allBooksRepo repository.BookRepo, bookshelfRepo repository.BookshelfRepo, log *slog.Logger) *BookService { +func NewBookService(repo repository.BookRepo, allBooksRepo repository.BookRepo, bookshelfRepo repository.BookshelfRepo, searcher *search.SearchService, log *slog.Logger) *BookService { return &BookService{ repo: repo, allBooksRepo: allBooksRepo, bookshelfRepo: bookshelfRepo, + searcher: searcher, log: log, bookLimit: 1000, // TODO: Read from config } } // Create creates a new book. -func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bool) error { +func (s *BookService) Create(ctx context.Context, book *models.Book) error { // Rule 2: Book Title & Author Presence if book.Title == "" || book.Author == "" { return ErrTitleAndAuthorRequired @@ -94,38 +98,6 @@ func (s *BookService) Create(ctx context.Context, book *models.Book, addToAll bo return fmt.Errorf("failed to create book: %w", err) } - // Добавляем книгу в allBooksRepo, если addToAll == true - if addToAll { - // Проверяем, существует ли книга по ISBN - _, err := s.allBooksRepo.GetByISBN(ctx, book.ISBN) // Используем GetByISBN - if err != nil && !errors.Is(err, mongo.ErrBookNotFound) { - s.log.Error("failed to check for existing book in allBooksRepo", slog.Any("error", err)) - return fmt.Errorf("failed to check for existing book in allBooksRepo: %w", err) - } - - if errors.Is(err, mongo.ErrBookNotFound) { - // Книги нет в allBooksRepo, можно добавить - allBook := &models.Book{ - Title: book.Title, - Author: book.Author, - ISBN: book.ISBN, - Publishing: book.Publishing, - Description: book.Description, - CoverImage: book.CoverImage, - ShopName: book.ShopName, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - // UserID и BookshelfID не устанавливаем - } - - if err := s.allBooksRepo.Create(ctx, allBook); err != nil { - s.log.Error("failed to create book in allBooksRepo", slog.Any("error", err)) - return ErrCantAddToAllBooks - } - } - // Если книга уже существует, ничего не делаем - } - return nil } @@ -248,3 +220,52 @@ func (s *BookService) Delete(ctx context.Context, bookID string) error { return nil } + +func (s *BookService) AddAdvanced(ctx context.Context, isbn string, index int) error { + resp, err := s.searcher.Advanced(ctx, isbn) + if err != nil { + if errors.Is(err, search.ErrISBNNotFound) { + return fmt.Errorf("nothing found by isbn: %w", err) + } + s.log.Error("failed to search", slog.Any("error", err)) + return fmt.Errorf("failed to search: %w", err) + } + + if index >= len(resp.Books) { + return errors.New("invalid index") + } + + // Добавляем книгу в allBooksRepo + // Проверяем, существует ли книга по ISBN + _, err = s.allBooksRepo.GetByISBN(ctx, isbn) + if errors.Is(err, mongo.ErrBookNotFound) { + // Книги нет в allBooksRepo, можно добавить + book := resp.Books[index] + allBook := &models.Book{ + Title: book.Title, + Author: book.Author, + ISBN: book.ISBN, + Publishing: book.Publishing, + Description: book.Description, + CoverImage: book.CoverImage, + ShopName: book.ShopName, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + // UserID и BookshelfID не устанавливаем + } + + if err := s.allBooksRepo.Create(ctx, allBook); err != nil { + s.log.Error("failed to create book in allBooksRepo", slog.Any("error", err)) + return ErrCantAddToAllBooks + } + + return nil + } + if err != nil { + s.log.Error("failed to check for existing book in allBooksRepo", slog.Any("error", err)) + return fmt.Errorf("failed to check for existing book in allBooksRepo: %w", err) + } + + return ErrBookAlreadyExistsInALL + // Если книга уже существует, ничего не делаем +} From d054614c548d67d8212c05cea5cbee7ab082af58 Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 17 Jul 2024 16:34:20 +0300 Subject: [PATCH 48/56] quick fix routes --- internal/server/routes/routes.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index dd92272..491e1d7 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -19,16 +19,6 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { api.GET("/health", handlers.HealthCheck) - // Bookshelf routes - bookshelvesGroup := api.Group("/bookshelves") - { - bookshelvesGroup.POST("/add", middlewares.AuthMiddleware(), h.Bookshelves.Create) - bookshelvesGroup.GET("/", middlewares.AuthMiddleware(), h.Bookshelves.GetByUser) - bookshelvesGroup.GET("/:id", middlewares.AuthMiddleware(), h.Bookshelves.GetByID) - bookshelvesGroup.PUT("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Update) - bookshelvesGroup.DELETE("/:id", middlewares.AuthMiddleware(), h.Bookshelves.Delete) - } - // Book routes booksGroup := api.Group("/books") { From ebeeddaee579c4b8aa03355b35db8e244d05889e Mon Sep 17 00:00:00 2001 From: Den Date: Wed, 17 Jul 2024 23:33:45 +0300 Subject: [PATCH 49/56] add missing rote --- internal/server/routes/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 491e1d7..7a58d79 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -23,6 +23,7 @@ func SetupRoutes(router *gin.Engine, h *Handlers) { booksGroup := api.Group("/books") { booksGroup.POST("/add", middlewares.AuthMiddleware(), h.Books.Create) + booksGroup.POST("/add/advanced", middlewares.AuthMiddleware(), h.Books.AddAdvanced) booksGroup.GET("/", middlewares.AuthMiddleware(), h.Books.GetByUser) booksGroup.GET("/:id", middlewares.AuthMiddleware(), h.Books.GetByID) booksGroup.GET("/isbn/:isbn", middlewares.AuthMiddleware(), h.Books.GetByISBN) From a5ecd97169dd6ecf6a38b576713398dbb3518587 Mon Sep 17 00:00:00 2001 From: Den Date: Thu, 18 Jul 2024 20:16:07 +0300 Subject: [PATCH 50/56] add bookshelf id to book update --- internal/server/models/book.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/server/models/book.go b/internal/server/models/book.go index 304ab8e..04c9de4 100644 --- a/internal/server/models/book.go +++ b/internal/server/models/book.go @@ -25,7 +25,8 @@ type Book struct { // BookUpdate represents fields that can be updated in a Book. type BookUpdate struct { ISBN *string `bson:"isbn,omitempty" json:"isbn,omitempty"` - Title *string `bson:"title,omitempty" json:"title,omitempty"` // Optional fields for update + BookshelfID *string `bson:"bookshelf_id,omitempty" json:"bookshelf_id,omitempty"` + Title *string `bson:"title,omitempty" json:"title,omitempty"` Author *string `bson:"author,omitempty" json:"author,omitempty"` Publishing *string `bson:"publishing" json:"publishing"` Description *string `bson:"description,omitempty" json:"description,omitempty"` From 735cbc61eb73207b5924d1f09a3a91143c858c3c Mon Sep 17 00:00:00 2001 From: Den Date: Sat, 20 Jul 2024 23:55:00 +0300 Subject: [PATCH 51/56] added just a couple of tests --- internal/server/services/book/book_test.go | 510 +++++++++++++++++++++ 1 file changed, 510 insertions(+) create mode 100644 internal/server/services/book/book_test.go diff --git a/internal/server/services/book/book_test.go b/internal/server/services/book/book_test.go new file mode 100644 index 0000000..8a30bd6 --- /dev/null +++ b/internal/server/services/book/book_test.go @@ -0,0 +1,510 @@ +package book + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/services/search" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "log/slog" + "os" + "testing" + "time" +) + +// MockRepository is a mock implementation of the repository.BookRepo interface. +type MockRepository struct { + mock.Mock +} + +// Create mocks the Create method of the BookRepo interface. +func (m *MockRepository) Create(ctx context.Context, book *models.Book) error { + args := m.Called(ctx, book) + return args.Error(0) +} + +// GetByID mocks the GetByID method of the BookRepo interface. +func (m *MockRepository) GetByID(ctx context.Context, id string) (*models.Book, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Book), args.Error(1) +} + +// GetByISBN mocks the GetByISBN method of the BookRepo interface. +func (m *MockRepository) GetByISBN(ctx context.Context, isbn string) (*models.Book, error) { + args := m.Called(ctx, isbn) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Book), args.Error(1) +} + +// GetByUserID mocks the GetByUserID method of the BookRepo interface. +func (m *MockRepository) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { + args := m.Called(ctx, userID, page, limit) + return args.Get(0).([]*models.Book), args.Error(1) +} + +// GetByBookshelfID mocks the GetByBookshelfID method of the BookRepo interface. +func (m *MockRepository) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { + args := m.Called(ctx, bookshelfID, page, limit) + return args.Get(0).([]*models.Book), args.Error(1) +} + +// CountInBookshelf mocks the CountInBookshelf method of the BookRepo interface. +func (m *MockRepository) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) { + args := m.Called(ctx, bookshelfID) + return args.Int(0), args.Error(1) +} + +// ExistsInBookshelf mocks the ExistsInBookshelf method of the BookRepo interface. +func (m *MockRepository) ExistsInBookshelf(ctx context.Context, isbn, bookshelfID string) (bool, error) { + args := m.Called(ctx, isbn, bookshelfID) + return args.Bool(0), args.Error(1) +} + +// Update mocks the Update method of the BookRepo interface. +func (m *MockRepository) Update(ctx context.Context, id string, update *models.BookUpdate) error { + args := m.Called(ctx, id, update) + return args.Error(0) +} + +// Delete mocks the Delete method of the BookRepo interface. +func (m *MockRepository) Delete(ctx context.Context, id string) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +// MockBookshelfRepository is a mock implementation of the repository.BookshelfRepo interface. +type MockBookshelfRepository struct { + mock.Mock +} + +// Create mocks the Create method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Create(ctx context.Context, bookshelf *models.Bookshelf) error { + args := m.Called(ctx, bookshelf) + return args.Error(0) +} + +// GetByID mocks the GetByID method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) GetByID(ctx context.Context, id string) (*models.Bookshelf, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Bookshelf), args.Error(1) +} + +// GetByUser mocks the GetByUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { + args := m.Called(ctx, userID, page, limit) + return args.Get(0).([]*models.Bookshelf), args.Error(1) +} + +// CountByUser mocks the CountByUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) CountByUser(ctx context.Context, userID string) (int, error) { + args := m.Called(ctx, userID) + return args.Int(0), args.Error(1) +} + +// ExistsByNameAndUser mocks the ExistsByNameAndUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) ExistsByNameAndUser(ctx context.Context, name, userID string) (bool, error) { + args := m.Called(ctx, name, userID) + return args.Bool(0), args.Error(1) +} + +// Update mocks the Update method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error { + args := m.Called(ctx, id, update) + return args.Error(0) +} + +// Delete mocks the Delete method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Delete(ctx context.Context, id string) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +// Helper function to create a pointer to a string +func stringPtr(s string) *string { + return &s +} + +// -- Tests -- // + +func TestBookService_Create_Success(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler( + //io.Discard, + //nil, + os.Stdout, + nil, + )) + service := &BookService{ + repo: repo, + allBooksRepo: repo, // In this test, both repos are the same mock + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + + book := &models.Book{ + UserID: userID, + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + Author: "Test Author", + } + + // Mock the bookshelfRepo.GetByID to return a valid bookshelf + bookshelfRepo.On("GetByID", ctx, book.BookshelfID).Return( + &models.Bookshelf{ + ID: book.BookshelfID, + UserID: userID, + }, nil, + ) + + // Mock the repo.CountInBookshelf to return a count less than the limit + repo.On("CountInBookshelf", ctx, book.BookshelfID).Return(500, nil) + + // Mock the repo.ExistsInBookshelf to return false (book doesn't exist) + repo.On("ExistsInBookshelf", ctx, book.ISBN, book.BookshelfID).Return(false, nil) + + // Mock the repo.Create to return no error + repo.On("Create", ctx, book).Return(nil) + + err := service.Create(ctx, book) + assert.NoError(t, err) + + repo.AssertExpectations(t) + bookshelfRepo.AssertExpectations(t) +} + +func TestBookService_Create_ErrorTitleAndAuthorRequired(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + // Test cases for missing title and/or author + testCases := []struct { + name string + book *models.Book + error error + }{ + { + name: "Missing Title", + book: &models.Book{ + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Author: "Test Author", + }, + error: ErrTitleAndAuthorRequired, + }, + { + name: "Missing Author", + book: &models.Book{ + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + }, + error: ErrTitleAndAuthorRequired, + }, + { + name: "Missing Title and Author", + book: &models.Book{ + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: "1234567890", + }, + error: ErrTitleAndAuthorRequired, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + + err := service.Create(ctx, tc.book) + assert.ErrorIs(t, err, tc.error) + }) + } + + repo.AssertExpectations(t) + bookshelfRepo.AssertExpectations(t) +} + +func TestBookService_Create_ErrorUserNotFoundInContext(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + + book := &models.Book{ + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + Author: "Test Author", + } + + err := service.Create(ctx, book) + assert.ErrorIs(t, err, ErrUserNotFoundInContext) + + repo.AssertExpectations(t) + bookshelfRepo.AssertExpectations(t) +} + +func TestBookService_GetByID_Success(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, // В этом тесте, allBooksRepo - тот же мок + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + bookID := "testbookid" + + expectedBook := &models.Book{ + ID: bookID, + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + Author: "Test Author", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + repo.On("GetByID", ctx, bookID).Return(expectedBook, nil) + + book, err := service.GetByID(ctx, bookID) + + assert.NoError(t, err) + assert.Equal(t, expectedBook, book) + repo.AssertExpectations(t) +} + +func TestBookService_GetByID_ErrorBookNotFound(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + bookID := "nonexistentbookid" + + repo.On("GetByID", ctx, bookID).Return(nil, mongo.ErrBookNotFound) + + book, err := service.GetByID(ctx, bookID) + + assert.ErrorIs(t, err, ErrBookNotFound) + assert.Nil(t, book) + repo.AssertExpectations(t) +} + +func TestBookService_Update_Success(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookID := "testbookid" + update := &models.BookUpdate{ + Title: stringPtr("Updated Title"), + Author: stringPtr("Updated Author"), + Publishing: stringPtr("Updated Publishing"), + Description: stringPtr("Updated Description"), + CoverImage: stringPtr("Updated CoverImage"), + ShopName: stringPtr("Updated ShopName"), + } + + existingBook := &models.Book{ + ID: bookID, + UserID: userID, + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + Author: "Test Author", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + bookshelfRepo.On("GetByID", ctx, existingBook.BookshelfID).Return( + &models.Bookshelf{ + ID: existingBook.BookshelfID, + UserID: userID, + }, nil, + ) + + repo.On("GetByID", ctx, bookID).Return(existingBook, nil) + repo.On("Update", ctx, bookID, update).Return(nil) + + err := service.Update(ctx, bookID, update) + + assert.NoError(t, err) + repo.AssertExpectations(t) + bookshelfRepo.AssertExpectations(t) +} + +func TestBookService_Update_ErrorBookNotFound(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookID := "nonexistentbookid" + update := &models.BookUpdate{ + Title: stringPtr("Updated Title"), + } + + repo.On("GetByID", ctx, bookID).Return(nil, mongo.ErrBookNotFound) + + err := service.Update(ctx, bookID, update) + + assert.ErrorIs(t, err, ErrBookNotFound) + repo.AssertExpectations(t) +} + +func TestBookService_Delete_Success(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookID := "testbookid" + + existingBook := &models.Book{ + ID: bookID, + UserID: userID, + BookshelfID: "testbookshelf", + ISBN: "1234567890", + Title: "Test Book", + Author: "Test Author", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + bookshelfRepo.On("GetByID", ctx, existingBook.BookshelfID).Return( + &models.Bookshelf{ + ID: existingBook.BookshelfID, + UserID: userID, + }, nil, + ) + + repo.On("GetByID", ctx, bookID).Return(existingBook, nil) + repo.On("Delete", ctx, bookID).Return(nil) + + err := service.Delete(ctx, bookID) + + assert.NoError(t, err) + repo.AssertExpectations(t) + bookshelfRepo.AssertExpectations(t) +} + +func TestBookService_Delete_ErrorBookNotFound(t *testing.T) { + repo := new(MockRepository) + bookshelfRepo := new(MockBookshelfRepository) + searcher := new(search.SearchService) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookService{ + repo: repo, + allBooksRepo: repo, + bookshelfRepo: bookshelfRepo, + searcher: searcher, + log: log, + bookLimit: 1000, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookID := "nonexistentbookid" + + repo.On("GetByID", ctx, bookID).Return(nil, mongo.ErrBookNotFound) + + err := service.Delete(ctx, bookID) + + assert.ErrorIs(t, err, ErrBookNotFound) + repo.AssertExpectations(t) +} From a32808caacff1f0e036268da0ad56b122d6c6890 Mon Sep 17 00:00:00 2001 From: Den Date: Sun, 21 Jul 2024 01:13:06 +0300 Subject: [PATCH 52/56] add bookshelf service tests --- .../services/bookshelf/bookshelf_test.go | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 internal/server/services/bookshelf/bookshelf_test.go diff --git a/internal/server/services/bookshelf/bookshelf_test.go b/internal/server/services/bookshelf/bookshelf_test.go new file mode 100644 index 0000000..d351373 --- /dev/null +++ b/internal/server/services/bookshelf/bookshelf_test.go @@ -0,0 +1,290 @@ +package bookshelf + +import ( + "context" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/getz-devs/librakeeper-server/internal/server/storage/mongo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "log/slog" + "os" + "testing" + "time" +) + +// MockBookshelfRepository is a mock implementation of the repository.BookshelfRepo interface. +type MockBookshelfRepository struct { + mock.Mock +} + +// Create mocks the Create method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Create(ctx context.Context, bookshelf *models.Bookshelf) error { + args := m.Called(ctx, bookshelf) + return args.Error(0) +} + +// GetByID mocks the GetByID method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) GetByID(ctx context.Context, id string) (*models.Bookshelf, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Bookshelf), args.Error(1) +} + +// GetByUser mocks the GetByUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) GetByUser(ctx context.Context, userID string, page int64, limit int64) ([]*models.Bookshelf, error) { + args := m.Called(ctx, userID, page, limit) + return args.Get(0).([]*models.Bookshelf), args.Error(1) +} + +// CountByUser mocks the CountByUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) CountByUser(ctx context.Context, userID string) (int, error) { + args := m.Called(ctx, userID) + return args.Int(0), args.Error(1) +} + +// ExistsByNameAndUser mocks the ExistsByNameAndUser method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) ExistsByNameAndUser(ctx context.Context, name, userID string) (bool, error) { + args := m.Called(ctx, name, userID) + return args.Bool(0), args.Error(1) +} + +// Update mocks the Update method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Update(ctx context.Context, id string, update *models.BookshelfUpdate) error { + args := m.Called(ctx, id, update) + return args.Error(0) +} + +// Delete mocks the Delete method of the BookshelfRepo interface. +func (m *MockBookshelfRepository) Delete(ctx context.Context, id string) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func TestBookshelfService_Create_Success(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + + bookshelf := &models.Bookshelf{ + Name: "Test Bookshelf", + } + + repo.On("ExistsByNameAndUser", ctx, bookshelf.Name, userID).Return(false, nil) + repo.On("Create", ctx, bookshelf).Return(nil) + + err := service.Create(ctx, bookshelf) + assert.NoError(t, err) + assert.Equal(t, userID, bookshelf.UserID) + + repo.AssertExpectations(t) +} + +func TestBookshelfService_Create_ErrorNameRequired(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + + bookshelf := &models.Bookshelf{} + + err := service.Create(ctx, bookshelf) + + assert.ErrorIs(t, err, ErrNameRequired) + repo.AssertExpectations(t) +} + +func TestBookshelfService_Create_ErrorUserNotFoundInContext(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + + bookshelf := &models.Bookshelf{ + Name: "Test Bookshelf", + } + + err := service.Create(ctx, bookshelf) + + assert.ErrorIs(t, err, ErrUserNotFoundInContext) + repo.AssertExpectations(t) +} + +func TestBookshelfService_GetByID_Success(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + bookshelfID := "testbookshelfid" + + expectedBookshelf := &models.Bookshelf{ + ID: bookshelfID, + UserID: "testuser", + Name: "Test Bookshelf", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + repo.On("GetByID", ctx, bookshelfID).Return(expectedBookshelf, nil) + + bookshelf, err := service.GetByID(ctx, bookshelfID) + + assert.NoError(t, err) + assert.Equal(t, expectedBookshelf, bookshelf) + repo.AssertExpectations(t) +} + +func TestBookshelfService_GetByID_ErrorBookshelfNotFound(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + bookshelfID := "nonexistentbookshelf" + + repo.On("GetByID", ctx, bookshelfID).Return(nil, mongo.ErrBookshelfNotFound) + + bookshelf, err := service.GetByID(ctx, bookshelfID) + + assert.ErrorIs(t, err, ErrBookshelfNotFound) + assert.Nil(t, bookshelf) + repo.AssertExpectations(t) +} + +func TestBookshelfService_Update_Success(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookshelfID := "testbookshelfid" + update := &models.BookshelfUpdate{ + Name: stringPtr("Updated Bookshelf Name"), + } + + existingBookshelf := &models.Bookshelf{ + ID: bookshelfID, + UserID: userID, + Name: "Test Bookshelf", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + repo.On("GetByID", ctx, bookshelfID).Return(existingBookshelf, nil) + repo.On("Update", ctx, bookshelfID, update).Return(nil) + + err := service.Update(ctx, bookshelfID, update) + + assert.NoError(t, err) + repo.AssertExpectations(t) +} + +func TestBookshelfService_Update_ErrorBookshelfNotFound(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookshelfID := "nonexistentbookshelf" + update := &models.BookshelfUpdate{ + Name: stringPtr("Updated Bookshelf Name"), + } + + repo.On("GetByID", ctx, bookshelfID).Return(nil, mongo.ErrBookshelfNotFound) + + err := service.Update(ctx, bookshelfID, update) + + assert.ErrorIs(t, err, ErrBookshelfNotFound) + repo.AssertExpectations(t) +} + +func TestBookshelfService_Delete_Success(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookshelfID := "testbookshelfid" + + existingBookshelf := &models.Bookshelf{ + ID: bookshelfID, + UserID: userID, + Name: "Test Bookshelf", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + repo.On("GetByID", ctx, bookshelfID).Return(existingBookshelf, nil) + repo.On("Delete", ctx, bookshelfID).Return(nil) + + err := service.Delete(ctx, bookshelfID) + + assert.NoError(t, err) + repo.AssertExpectations(t) +} + +func TestBookshelfService_Delete_ErrorBookshelfNotFound(t *testing.T) { + repo := new(MockBookshelfRepository) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &BookshelfService{ + repo: repo, + log: log, + } + + ctx := context.Background() + userID := "testuser" + ctx = context.WithValue(ctx, "userID", userID) + bookshelfID := "nonexistentbookshelf" + + repo.On("GetByID", ctx, bookshelfID).Return(nil, mongo.ErrBookshelfNotFound) + + err := service.Delete(ctx, bookshelfID) + + assert.ErrorIs(t, err, ErrBookshelfNotFound) + repo.AssertExpectations(t) +} + +// Helper function to create a pointer to a string +func stringPtr(s string) *string { + return &s +} From 26fbc1394a4cc15afc08cc2eec630e95f0ebe212 Mon Sep 17 00:00:00 2001 From: Den Date: Sun, 21 Jul 2024 01:46:32 +0300 Subject: [PATCH 53/56] final tests --- go.mod | 1 + go.sum | 1 + .../server/services/search/search_test.go | 231 ++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 internal/server/services/search/search_test.go diff --git a/go.mod b/go.mod index 402e8fb..f4281fd 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/temoto/robotstxt v1.1.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/go.sum b/go.sum index 27b4f19..b97352b 100644 --- a/go.sum +++ b/go.sum @@ -173,6 +173,7 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/server/services/search/search_test.go b/internal/server/services/search/search_test.go new file mode 100644 index 0000000..7a028ec --- /dev/null +++ b/internal/server/services/search/search_test.go @@ -0,0 +1,231 @@ +package search + +import ( + "context" + "errors" + searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" + "github.com/getz-devs/librakeeper-server/internal/server/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "log/slog" + "os" + "testing" + "time" +) + +type MockSearchRepo struct { + mock.Mock +} + +func (m *MockSearchRepo) SearchByISBN(ctx context.Context, isbn string) (*searcherv1.SearchByISBNResponse, error) { + args := m.Called(ctx, isbn) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*searcherv1.SearchByISBNResponse), args.Error(1) +} + +type MockBookRepo struct { + mock.Mock +} + +func (m *MockBookRepo) Create(ctx context.Context, book *models.Book) error { + args := m.Called(ctx, book) + return args.Error(0) +} + +func (m *MockBookRepo) GetByID(ctx context.Context, id string) (*models.Book, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Book), args.Error(1) +} + +func (m *MockBookRepo) GetByISBN(ctx context.Context, isbn string) (*models.Book, error) { + args := m.Called(ctx, isbn) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Book), args.Error(1) +} + +func (m *MockBookRepo) GetByUserID(ctx context.Context, userID string, page int64, limit int64) ([]*models.Book, error) { + args := m.Called(ctx, userID, page, limit) + return args.Get(0).([]*models.Book), args.Error(1) +} + +func (m *MockBookRepo) GetByBookshelfID(ctx context.Context, bookshelfID string, page int64, limit int64) ([]*models.Book, error) { + args := m.Called(ctx, bookshelfID, page, limit) + return args.Get(0).([]*models.Book), args.Error(1) +} + +func (m *MockBookRepo) CountInBookshelf(ctx context.Context, bookshelfID string) (int, error) { + args := m.Called(ctx, bookshelfID) + return args.Int(0), args.Error(1) +} + +func (m *MockBookRepo) ExistsInBookshelf(ctx context.Context, isbn, bookshelfID string) (bool, error) { + args := m.Called(ctx, isbn, bookshelfID) + return args.Bool(0), args.Error(1) +} + +func (m *MockBookRepo) Update(ctx context.Context, id string, update *models.BookUpdate) error { + args := m.Called(ctx, id, update) + return args.Error(0) +} + +func (m *MockBookRepo) Delete(ctx context.Context, id string) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func TestSearchService_Simple_Success(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "1234567890" + expectedBook := &models.Book{ + ID: "testbookid", + UserID: "testuser", + BookshelfID: "testbookshelf", + ISBN: isbn, + Title: "Test Book", + Author: "Test Author", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + bookRepo.On("GetByISBN", ctx, isbn).Return(expectedBook, nil) + + resp, err := service.Simple(ctx, isbn) + + assert.NoError(t, err) + assert.Equal(t, searcherv1.SearchByISBNResponse_SUCCESS, resp.Status) + assert.Equal(t, expectedBook, resp.Books[0]) + bookRepo.AssertExpectations(t) +} + +func TestSearchService_Simple_ErrorISBNNotFound(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "nonexistentisbn" + + bookRepo.On("GetByISBN", ctx, isbn).Return(nil, errors.New("isbn not found")) + + _, err := service.Simple(ctx, isbn) + assert.ErrorContains(t, err, "isbn not found") + bookRepo.AssertExpectations(t) +} + +func TestSearchService_Simple_ErrorISBNRequired(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "" + + _, err := service.Simple(ctx, isbn) + assert.ErrorIs(t, err, ErrISBNRequired) +} + +func TestSearchService_Advanced_Success(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "1234567890" + + grpcResponse := &searcherv1.SearchByISBNResponse{ + Status: searcherv1.SearchByISBNResponse_SUCCESS, + Books: []*searcherv1.Book{ + { + Title: "Test Book 1", + Author: "Test Author 1", + Publishing: "Test Publishing 1", + ImgUrl: "https://example.com/cover1.jpg", + ShopName: "Test Shop 1", + }, + { + Title: "Test Book 2", + Author: "Test Author 2", + Publishing: "Test Publishing 2", + ImgUrl: "https://example.com/cover2.jpg", + ShopName: "Test Shop 2", + }, + }, + } + + searchRepo.On("SearchByISBN", ctx, isbn).Return(grpcResponse, nil) + + resp, err := service.Advanced(ctx, isbn) + + assert.NoError(t, err) + assert.Equal(t, grpcResponse.Status, resp.Status) + assert.Len(t, resp.Books, len(grpcResponse.Books)) + searchRepo.AssertExpectations(t) +} + +func TestSearchService_Advanced_ErrorISBNNotFound(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "nonexistentisbn" + + searchRepo.On("SearchByISBN", ctx, isbn).Return(nil, errors.New("isbn not found")) + + _, err := service.Advanced(ctx, isbn) + assert.ErrorContains(t, err, "isbn not found") + searchRepo.AssertExpectations(t) +} + +func TestSearchService_Advanced_ErrorISBNRequired(t *testing.T) { + searchRepo := new(MockSearchRepo) + bookRepo := new(MockBookRepo) + log := slog.New(slog.NewTextHandler(os.Stdout, nil)) + service := &SearchService{ + searcher: searchRepo, + allBooksRepo: bookRepo, + log: log, + } + + ctx := context.Background() + isbn := "" + + _, err := service.Advanced(ctx, isbn) + assert.ErrorIs(t, err, ErrISBNRequired) +} From 6334c9643d44b89a178e6fd7696292a1f4f382c4 Mon Sep 17 00:00:00 2001 From: Den Date: Sun, 21 Jul 2024 01:57:22 +0300 Subject: [PATCH 54/56] remove old code --- cmd/test_main/test_main.go | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 cmd/test_main/test_main.go diff --git a/cmd/test_main/test_main.go b/cmd/test_main/test_main.go deleted file mode 100644 index f80ea2a..0000000 --- a/cmd/test_main/test_main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "github.com/getz-devs/librakeeper-server/internal/searcher-agent/rabbit" -) - -func main() { - _, err := rabbit.ScrapISBNFindBook("9785206000344") - if err != nil { - panic(err) - } -} From 3167a0b374dc2ed40a4c076536229f9b7d1065b1 Mon Sep 17 00:00:00 2001 From: Den Date: Sun, 21 Jul 2024 03:09:29 +0300 Subject: [PATCH 55/56] graceful shutdown implemented --- cmd/server/main.go | 16 +++------ internal/server/server.go | 71 +++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3902c06..8d72b26 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,20 +10,12 @@ import ( func main() { cfg := config.MustLoad() - log := prettylog.SetupLogger(cfg.Env) - log.Info("starting librakeeper srv", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) - - // Create and initialize the srv - srv := server.NewServer(cfg, log) - if err := srv.Initialize(); err != nil { - log.Error("failed to initialize srv", slog.Any("error", err)) - os.Exit(1) - } + log.Info("starting librakeeper server", slog.String("env", cfg.Env), slog.Int("port", cfg.Server.Port)) - // Run the srv - if err := srv.Run(); err != nil { - log.Error("failed to run srv", slog.Any("error", err)) + // Create and run the server + if err := server.NewServer(cfg, log).Run(); err != nil { + log.Error("server error", slog.Any("error", err)) os.Exit(1) } } diff --git a/internal/server/server.go b/internal/server/server.go index c8b1901..2d350ca 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -38,12 +38,51 @@ func NewServer(config *config.Config, log *slog.Logger) *Server { return &Server{ config: config, log: log, - router: gin.New(), } } -// Initialize initializes the server components. -func (s *Server) Initialize() error { +// Run initializes and starts the HTTP server and handles graceful shutdown. +func (s *Server) Run() error { + if err := s.initialize(); err != nil { + return fmt.Errorf("failed to initialize server: %w", err) + } + + // Graceful Shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + s.log.Info("received shutdown signal") + cancel() + }() + + return s.runHTTPServer(ctx) +} + +func (s *Server) runHTTPServer(ctx context.Context) error { + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.log.Error("failed to start server", slog.Any("error", err)) + } + }() + + s.log.Info("server started successfully, press Ctrl+C to stop") + + <-ctx.Done() // Block until the context is canceled (shutdown signal) + + s.log.Info("shutting down server...") + + shutdownCtx, done := context.WithTimeout(ctx, 5*time.Second) + defer done() + + return s.httpServer.Shutdown(shutdownCtx) // Graceful shutdown +} + +// initialize initializes the server components. +func (s *Server) initialize() error { // Initialize Firebase err := auth.InitializeFirebase(s.config.Auth.ConfigPath) if err != nil { @@ -60,7 +99,7 @@ func (s *Server) Initialize() error { grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { - panic(err) + return fmt.Errorf("failed to connect to gRPC server: %w", err) } bookRepo := mongo.NewBookRepo(db, s.log, "user_books") @@ -80,11 +119,13 @@ func (s *Server) Initialize() error { // Configure CORS corsConfig := cors.Config{ - AllowOrigins: s.config.Server.AllowedOrigins, // Get origins from config + AllowOrigins: s.config.Server.AllowedOrigins, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, AllowCredentials: true, } + + s.router = gin.New() s.router.Use(gin.Logger(), gin.Recovery(), cors.New(corsConfig)) routes.SetupRoutes(s.router, h) @@ -95,23 +136,3 @@ func (s *Server) Initialize() error { return nil } - -// Run starts the HTTP server and handles graceful shutdown. -func (s *Server) Run() error { - go func() { - if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - s.log.Error("failed to start server", slog.Any("error", err)) - } - }() - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - s.log.Info("shutting down server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - return s.httpServer.Shutdown(ctx) -} From 82008714d76e31344a4c8bd12b6dc8de243cf937 Mon Sep 17 00:00:00 2001 From: Den Date: Sun, 21 Jul 2024 03:40:38 +0300 Subject: [PATCH 56/56] move integration tests to subfolder --- tests/{ => integration}/searcher/searcher_test.go | 2 +- tests/{ => integration}/searcher/suite/suite.go | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{ => integration}/searcher/searcher_test.go (95%) rename tests/{ => integration}/searcher/suite/suite.go (100%) diff --git a/tests/searcher/searcher_test.go b/tests/integration/searcher/searcher_test.go similarity index 95% rename from tests/searcher/searcher_test.go rename to tests/integration/searcher/searcher_test.go index 978de5f..e492f55 100644 --- a/tests/searcher/searcher_test.go +++ b/tests/integration/searcher/searcher_test.go @@ -2,7 +2,7 @@ package tests import ( searcherv1 "github.com/getz-devs/librakeeper-protos/gen/go/searcher" - "github.com/getz-devs/librakeeper-server/tests/searcher/suite" + "github.com/getz-devs/librakeeper-server/tests/integration/searcher/suite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "math/rand" diff --git a/tests/searcher/suite/suite.go b/tests/integration/searcher/suite/suite.go similarity index 100% rename from tests/searcher/suite/suite.go rename to tests/integration/searcher/suite/suite.go