From 6fd24f4435c7af4fc40e1d864fd44c16e105fab7 Mon Sep 17 00:00:00 2001 From: PickHD Date: Mon, 8 May 2023 18:58:28 +0700 Subject: [PATCH 1/4] chore (SRS-06) : add timestamp to users collections --- auth/internal/v1/model/user.go | 15 ++++++++++----- auth/internal/v1/repository/auth.go | 8 +++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/auth/internal/v1/model/user.go b/auth/internal/v1/model/user.go index 31c30a9..afd604c 100644 --- a/auth/internal/v1/model/user.go +++ b/auth/internal/v1/model/user.go @@ -1,13 +1,18 @@ package model -import "go.mongodb.org/mongo-driver/bson/primitive" +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) type ( // User consist data of users User struct { - ID primitive.ObjectID `bson:"_id,omitempty"` - FullName string `bson:"fullname,omitempty"` - Email string `bson:"email,omitempty"` - Password string `bson:"password,omitempty"` + ID primitive.ObjectID `bson:"_id,omitempty"` + FullName string `bson:"fullname,omitempty"` + Email string `bson:"email,omitempty"` + Password string `bson:"password,omitempty"` + CreatedAt time.Time `bson:"created_at,omitempty"` } ) diff --git a/auth/internal/v1/repository/auth.go b/auth/internal/v1/repository/auth.go index 4b5cfcb..49c5d24 100644 --- a/auth/internal/v1/repository/auth.go +++ b/auth/internal/v1/repository/auth.go @@ -2,6 +2,7 @@ package repository import ( "context" + "time" "github.com/PickHD/singkatin-revamp/auth/internal/v1/config" "github.com/PickHD/singkatin-revamp/auth/internal/v1/model" @@ -44,9 +45,10 @@ func (ar *AuthRepositoryImpl) CreateUser(ctx context.Context, req *model.User) ( // if doc not exists, create new one if err == mongo.ErrNoDocuments { res, err := ar.DB.Collection(ar.Config.Database.UsersCollection).InsertOne(ctx, model.User{ - FullName: req.FullName, - Email: req.Email, - Password: req.Password, + FullName: req.FullName, + Email: req.Email, + Password: req.Password, + CreatedAt: time.Now(), }) if err != nil { ar.Logger.Error("AuthRepositoryImpl.CreateUser InsertOne ERROR, ", err) From 63ebd141e32c297dce79f4b3ee4b224bb64294be Mon Sep 17 00:00:00 2001 From: PickHD Date: Mon, 8 May 2023 18:59:18 +0700 Subject: [PATCH 2/4] feat (SRS-06) : improve generic consumer queues --- shortener/cmd/v1/main.go | 11 +++++++- .../internal/v1/infrastructure/rabbitmq.go | 27 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/shortener/cmd/v1/main.go b/shortener/cmd/v1/main.go index 29f7027..365db9e 100644 --- a/shortener/cmd/v1/main.go +++ b/shortener/cmd/v1/main.go @@ -138,6 +138,15 @@ func main() { app.Logger.Error("Error received by channel", err) } case consumerMode: - infrastructure.ConsumeMessages(app, app.Config.RabbitMQ.QueueCreateShortener) + // Make a channel to receive messages into infinite loop. + forever := make(chan bool) + + queues := []string{app.Config.RabbitMQ.QueueCreateShortener, app.Config.RabbitMQ.QueueUpdateVisitor} + + for _, q := range queues { + go infrastructure.ConsumeMessages(app, q) + } + + <-forever } } diff --git a/shortener/internal/v1/infrastructure/rabbitmq.go b/shortener/internal/v1/infrastructure/rabbitmq.go index a2d35b9..f791d9a 100644 --- a/shortener/internal/v1/infrastructure/rabbitmq.go +++ b/shortener/internal/v1/infrastructure/rabbitmq.go @@ -2,11 +2,13 @@ package infrastructure import ( "encoding/json" + "fmt" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/application" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/model" ) +// ConsumeMessages generic function to consume message from defined param queues func ConsumeMessages(app *application.App, queueName string) { dep := application.SetupDependencyInjection(app) @@ -26,9 +28,6 @@ func ConsumeMessages(app *application.App, queueName string) { app.Logger.Info("Waiting Message in Queues ", queueName, ".....") - // Make a channel to receive messages into infinite loop. - forever := make(chan bool) - go func() { for msg := range messages { switch queueName { @@ -40,18 +39,32 @@ func ConsumeMessages(app *application.App, queueName string) { app.Logger.Error("Unmarshal JSON ERROR, ", err) } - app.Logger.Info("Success Consume Message :", req) + app.Logger.Info(fmt.Sprintf("[%s] Success Consume Message :", queueName), req) err = dep.ShortController.ProcessCreateShortUser(app.Context, &req) if err != nil { app.Logger.Error("ProcessCreateShortUser ERROR, ", err) } - app.Logger.Info("Success Process Message : ", req) + app.Logger.Info(fmt.Sprintf("[%s] Success Process Message :", queueName), req) case app.Config.RabbitMQ.QueueUpdateVisitor: + var req model.UpdateVisitorRequest + + err := json.Unmarshal(msg.Body, &req) + if err != nil { + app.Logger.Error("Unmarshal JSON ERROR, ", err) + } + + app.Logger.Info(fmt.Sprintf("[%s] Success Consume Message :", queueName), req) + + err = dep.ShortController.ProcessUpdateVisitorCount(app.Context, &req) + if err != nil { + app.Logger.Error("ProcessUpdateVisitorCount ERROR, ", err) + } + + app.Logger.Info(fmt.Sprintf("[%s] Success Process Message :", queueName), req) + } } }() - - <-forever } From 68ec32d5d7adc39e425fd24b67d6e067fa86cc37 Mon Sep 17 00:00:00 2001 From: PickHD Date: Mon, 8 May 2023 18:59:32 +0700 Subject: [PATCH 3/4] feat (SRS-06) : update docs swagger --- shortener/docs/docs.go | 49 +++++++++++++++++++++++++++++++++++++ shortener/docs/swagger.json | 49 +++++++++++++++++++++++++++++++++++++ shortener/docs/swagger.yaml | 32 ++++++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/shortener/docs/docs.go b/shortener/docs/docs.go index d5649d7..d4bbae3 100644 --- a/shortener/docs/docs.go +++ b/shortener/docs/docs.go @@ -49,6 +49,55 @@ const docTemplate = `{ } } } + }, + "/{short_url}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shortener" + ], + "summary": "Click Shorteners URL", + "parameters": [ + { + "type": "string", + "description": "short urls", + "name": "short_url", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + } + } + } } }, "definitions": { diff --git a/shortener/docs/swagger.json b/shortener/docs/swagger.json index eff3890..fa581cb 100644 --- a/shortener/docs/swagger.json +++ b/shortener/docs/swagger.json @@ -45,6 +45,55 @@ } } } + }, + "/{short_url}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Shortener" + ], + "summary": "Click Shorteners URL", + "parameters": [ + { + "type": "string", + "description": "short urls", + "name": "short_url", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/helper.BaseResponse" + } + } + } + } } }, "definitions": { diff --git a/shortener/docs/swagger.yaml b/shortener/docs/swagger.yaml index 1d574ab..aac7717 100644 --- a/shortener/docs/swagger.yaml +++ b/shortener/docs/swagger.yaml @@ -29,6 +29,38 @@ info: title: Singkatin Revamp API version: "1.0" paths: + /{short_url}: + get: + consumes: + - application/json + parameters: + - description: short urls + in: path + name: short_url + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/helper.BaseResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/helper.BaseResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/helper.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/helper.BaseResponse' + summary: Click Shorteners URL + tags: + - Shortener /health-check: get: consumes: From 47681d0c5c3f1b23032ee958b7f05a314552c8cc Mon Sep 17 00:00:00 2001 From: PickHD Date: Mon, 8 May 2023 19:00:18 +0700 Subject: [PATCH 4/4] feat (SRS-06) : implement API click shorteners --- shortener/internal/v1/application/app.go | 2 +- .../internal/v1/application/dependency.go | 2 +- shortener/internal/v1/config/config.go | 2 +- shortener/internal/v1/controller/short.go | 48 +++++++ shortener/internal/v1/infrastructure/http.go | 2 + shortener/internal/v1/model/key.go | 5 + shortener/internal/v1/model/short.go | 26 +++- shortener/internal/v1/repository/short.go | 124 ++++++++++++++++-- shortener/internal/v1/service/short.go | 90 +++++++++++++ 9 files changed, 281 insertions(+), 20 deletions(-) create mode 100644 shortener/internal/v1/model/key.go diff --git a/shortener/internal/v1/application/app.go b/shortener/internal/v1/application/app.go index 1cab158..76f9e31 100644 --- a/shortener/internal/v1/application/app.go +++ b/shortener/internal/v1/application/app.go @@ -94,7 +94,7 @@ func SetupApplication(ctx context.Context) (*App, error) { app.Application = echo.New() app.Application.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, - AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, })) app.GRPC = grpc.NewServer() diff --git a/shortener/internal/v1/application/dependency.go b/shortener/internal/v1/application/dependency.go index 1df1c9f..3185638 100644 --- a/shortener/internal/v1/application/dependency.go +++ b/shortener/internal/v1/application/dependency.go @@ -14,7 +14,7 @@ type Dependency struct { func SetupDependencyInjection(app *App) *Dependency { // repository healthCheckRepoImpl := repository.NewHealthCheckRepository(app.Context, app.Config, app.Logger, app.DB, app.Redis) - shortRepoImpl := repository.NewShortRepository(app.Context, app.Config, app.Logger, app.DB) + shortRepoImpl := repository.NewShortRepository(app.Context, app.Config, app.Logger, app.DB, app.Redis, app.RabbitMQ) // service healthCheckSvcImpl := service.NewHealthCheckService(app.Context, app.Config, healthCheckRepoImpl) diff --git a/shortener/internal/v1/config/config.go b/shortener/internal/v1/config/config.go index 28eda6e..03546b3 100644 --- a/shortener/internal/v1/config/config.go +++ b/shortener/internal/v1/config/config.go @@ -57,7 +57,7 @@ func loadConfiguration() *Configuration { RabbitMQ: &RabbitMQ{ ConnURL: helper.GetEnvString("AMQP_SERVER_URL"), QueueCreateShortener: helper.GetEnvString("AMQP_QUEUE_CREATE_SHORTENER"), - QueueUpdateVisitor: helper.GetEnvString("AMQP_QUEUE_UPDATE_VISITOR"), + QueueUpdateVisitor: helper.GetEnvString("AMQP_QUEUE_UPDATE_VISITOR_COUNT"), }, } } diff --git a/shortener/internal/v1/controller/short.go b/shortener/internal/v1/controller/short.go index bb2ca87..8db9ad4 100644 --- a/shortener/internal/v1/controller/short.go +++ b/shortener/internal/v1/controller/short.go @@ -2,11 +2,15 @@ package controller import ( "context" + "net/http" + "strings" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/config" + "github.com/PickHD/singkatin-revamp/shortener/internal/v1/helper" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/model" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/service" shortenerpb "github.com/PickHD/singkatin-revamp/shortener/pkg/api/v1/proto/shortener" + "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,8 +19,15 @@ import ( type ( // ShortController is an interface that has all the function to be implemented inside short controller ShortController interface { + // grpc GetListShortenerByUserID(ctx context.Context, req *shortenerpb.ListShortenerRequest) (*shortenerpb.ListShortenerResponse, error) + + // http + ClickShortener(ctx echo.Context) error + + // rabbitmq ProcessCreateShortUser(ctx context.Context, req *model.CreateShortRequest) error + ProcessUpdateVisitorCount(ctx context.Context, req *model.UpdateVisitorRequest) error } // ShortControllerImpl is an app short struct that consists of all the dependencies needed for short controller @@ -65,6 +76,34 @@ func (sc *ShortControllerImpl) GetListShortenerByUserID(ctx context.Context, req }, nil } +// Check godoc +// @Summary Click Shorteners URL +// @Tags Shortener +// @Accept json +// @Produce json +// @Param short_url path string true "short urls" +// @Success 200 {object} helper.BaseResponse +// @Failure 400 {object} helper.BaseResponse +// @Failure 404 {object} helper.BaseResponse +// @Failure 500 {object} helper.BaseResponse +// @Router /{short_url} [get] +func (sc *ShortControllerImpl) ClickShortener(ctx echo.Context) error { + data, err := sc.ShortSvc.ClickShort(ctx.Param("short_url")) + if err != nil { + if strings.Contains(err.Error(), string(model.Validation)) { + return helper.NewResponses[any](ctx, http.StatusBadRequest, err.Error(), ctx.Param("short_url"), err, nil) + } + + if strings.Contains(err.Error(), string(model.NotFound)) { + return helper.NewResponses[any](ctx, http.StatusNotFound, err.Error(), ctx.Param("short_url"), err, nil) + } + + return helper.NewResponses[any](ctx, http.StatusInternalServerError, "failed click shortener", ctx.Param("short_url"), err, nil) + } + + return ctx.Redirect(http.StatusTemporaryRedirect, data.FullURL) +} + func (sc *ShortControllerImpl) ProcessCreateShortUser(ctx context.Context, req *model.CreateShortRequest) error { err := sc.ShortSvc.CreateShort(ctx, req) if err != nil { @@ -73,3 +112,12 @@ func (sc *ShortControllerImpl) ProcessCreateShortUser(ctx context.Context, req * return nil } + +func (sc *ShortControllerImpl) ProcessUpdateVisitorCount(ctx context.Context, req *model.UpdateVisitorRequest) error { + err := sc.ShortSvc.UpdateVisitorShort(ctx, req) + if err != nil { + return model.NewError(model.Internal, err.Error()) + } + + return nil +} diff --git a/shortener/internal/v1/infrastructure/http.go b/shortener/internal/v1/infrastructure/http.go index 0832b04..39753d2 100644 --- a/shortener/internal/v1/infrastructure/http.go +++ b/shortener/internal/v1/infrastructure/http.go @@ -24,6 +24,8 @@ func setupRouter(app *application.App) { v1.GET("/swagger/*any", echoSwagger.WrapHandler) v1.GET("/health-check", dep.HealthCheckController.Check) + + v1.GET("/:short_url", dep.ShortController.ClickShortener) } } diff --git a/shortener/internal/v1/model/key.go b/shortener/internal/v1/model/key.go new file mode 100644 index 0000000..ad9317d --- /dev/null +++ b/shortener/internal/v1/model/key.go @@ -0,0 +1,5 @@ +package model + +const ( + KeyShortURL = "short_url:%s" +) diff --git a/shortener/internal/v1/model/short.go b/shortener/internal/v1/model/short.go index b3aa1b5..3221e73 100644 --- a/shortener/internal/v1/model/short.go +++ b/shortener/internal/v1/model/short.go @@ -1,14 +1,20 @@ package model -import "go.mongodb.org/mongo-driver/bson/primitive" +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) type ( Short struct { - ID primitive.ObjectID `bson:"_id"` - UserID string `bson:"user_id"` - FullURL string `bson:"full_url"` - ShortURL string `bson:"short_url"` - Visited int64 `bson:"visited"` + ID primitive.ObjectID `bson:"_id"` + UserID string `bson:"user_id"` + FullURL string `bson:"full_url"` + ShortURL string `bson:"short_url"` + Visited int64 `bson:"visited"` + CreatedAt time.Time `bson:"created_at"` + UpdatedAt *time.Time `bson:"updated_at"` } CreateShortRequest struct { @@ -16,4 +22,12 @@ type ( FullURL string `json:"full_url"` ShortURL string `json:"short_url"` } + + ClickShortResponse struct { + FullURL string `json:"full_url"` + } + + UpdateVisitorRequest struct { + ShortURL string `json:"short_url"` + } ) diff --git a/shortener/internal/v1/repository/short.go b/shortener/internal/v1/repository/short.go index e2ec0eb..5278592 100644 --- a/shortener/internal/v1/repository/short.go +++ b/shortener/internal/v1/repository/short.go @@ -2,12 +2,18 @@ package repository import ( "context" + "encoding/json" + "fmt" + "time" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/config" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/model" + "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" + "github.com/streadway/amqp" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) type ( @@ -15,31 +21,42 @@ type ( ShortRepository interface { GetListShortenerByUserID(ctx context.Context, userID string) ([]model.Short, error) Create(ctx context.Context, req *model.Short) error + GetByShortURL(ctx context.Context, shortURL string) (*model.Short, error) + GetFullURLByKey(ctx context.Context, shortURL string) (string, error) + SetFullURLByKey(ctx context.Context, shortURL string, fullURL string, duration time.Duration) error + PublishUpdateVisitorCount(ctx context.Context, req *model.UpdateVisitorRequest) error + UpdateVisitorByShortURL(ctx context.Context, req *model.UpdateVisitorRequest, lastVisitedCount int64) error } // ShortRepositoryImpl is an app short struct that consists of all the dependencies needed for short repository ShortRepositoryImpl struct { - Context context.Context - Config *config.Configuration - Logger *logrus.Logger - DB *mongo.Database + Context context.Context + Config *config.Configuration + Logger *logrus.Logger + DB *mongo.Database + Redis *redis.Client + RabbitMQ *amqp.Channel } ) // NewShortRepository return new instances short repository -func NewShortRepository(ctx context.Context, config *config.Configuration, logger *logrus.Logger, db *mongo.Database) *ShortRepositoryImpl { +func NewShortRepository(ctx context.Context, config *config.Configuration, logger *logrus.Logger, db *mongo.Database, rds *redis.Client, amqp *amqp.Channel) *ShortRepositoryImpl { return &ShortRepositoryImpl{ - Context: ctx, - Config: config, - Logger: logger, - DB: db, + Context: ctx, + Config: config, + Logger: logger, + DB: db, + Redis: rds, + RabbitMQ: amqp, } } func (sr *ShortRepositoryImpl) GetListShortenerByUserID(ctx context.Context, userID string) ([]model.Short, error) { shorts := []model.Short{} - cur, err := sr.DB.Collection(sr.Config.Database.ShortenersCollection).Find(ctx, bson.D{{Key: "user_id", Value: userID}}) + cur, err := sr.DB.Collection(sr.Config.Database.ShortenersCollection).Find(ctx, + bson.D{{Key: "user_id", Value: userID}}, + options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}, {Key: "_id", Value: -1}})) if err != nil { sr.Logger.Error("ShortRepositoryImpl.GetListShortenerByUserID Find ERROR, ", err) return nil, err @@ -66,7 +83,10 @@ func (sr *ShortRepositoryImpl) GetListShortenerByUserID(ctx context.Context, use func (sr *ShortRepositoryImpl) Create(ctx context.Context, req *model.Short) error { _, err := sr.DB.Collection(sr.Config.Database.ShortenersCollection).InsertOne(ctx, - bson.D{{Key: "full_url", Value: req.FullURL}, {Key: "user_id", Value: req.UserID}, {Key: "short_url", Value: req.ShortURL}, {Key: "visited", Value: 0}}) + bson.D{{Key: "full_url", Value: req.FullURL}, + {Key: "user_id", Value: req.UserID}, + {Key: "short_url", Value: req.ShortURL}, + {Key: "visited", Value: 0}, {Key: "created_at", Value: time.Now()}}) if err != nil { sr.Logger.Error("ShortRepositoryImpl.Create InsertOne ERROR, ", err) return err @@ -74,3 +94,85 @@ func (sr *ShortRepositoryImpl) Create(ctx context.Context, req *model.Short) err return nil } + +func (sr *ShortRepositoryImpl) GetByShortURL(ctx context.Context, shortURL string) (*model.Short, error) { + short := &model.Short{} + + err := sr.DB.Collection(sr.Config.Database.ShortenersCollection).FindOne(ctx, bson.D{{Key: "short_url", Value: shortURL}}).Decode(&short) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, model.NewError(model.NotFound, "short_url not found") + } + + sr.Logger.Error("ShortRepositoryImpl.GetByShortURL FindOne ERROR,", err) + return nil, err + } + + return short, nil +} + +func (sr *ShortRepositoryImpl) GetFullURLByKey(ctx context.Context, shortURL string) (string, error) { + result := sr.Redis.Get(ctx, fmt.Sprintf(model.KeyShortURL, shortURL)) + if result.Err() != nil { + sr.Logger.Error("ShortRepositoryImpl.GetFullURLByKey Get ERROR, ", result.Err()) + + return "", result.Err() + } + + return result.String(), nil +} + +func (sr *ShortRepositoryImpl) SetFullURLByKey(ctx context.Context, shortURL string, fullURL string, duration time.Duration) error { + err := sr.Redis.SetEx(ctx, fmt.Sprintf(model.KeyShortURL, shortURL), fullURL, duration).Err() + if err != nil { + sr.Logger.Error("ShortRepositoryImpl.SetFullURLByKey SetEx ERROR, ", err) + + return err + } + + return nil +} + +func (sr *ShortRepositoryImpl) PublishUpdateVisitorCount(ctx context.Context, req *model.UpdateVisitorRequest) error { + sr.Logger.Info("data req before publish", req) + + b, err := json.Marshal(&req) + if err != nil { + sr.Logger.Error("ShortRepositoryImpl.PublishUpdateVisitorCount Marshal JSON ERROR, ", err) + return err + } + + message := amqp.Publishing{ + ContentType: "application/json", + Body: b, + } + + // Attempt to publish a message to the queue. + if err := sr.RabbitMQ.Publish( + "", // exchange + sr.Config.RabbitMQ.QueueUpdateVisitor, // queue name + false, // mandatory + false, // immediate + message, // message to publish + ); err != nil { + sr.Logger.Error("ShortRepositoryImpl.PublishUpdateVisitorCount RabbitMQ.Publish ERROR, ", err) + return err + } + + sr.Logger.Info("Success Publish Update Visitor Count to Queue: ", sr.Config.RabbitMQ.QueueUpdateVisitor) + + return nil +} + +func (sr *ShortRepositoryImpl) UpdateVisitorByShortURL(ctx context.Context, req *model.UpdateVisitorRequest, lastVisitedCount int64) error { + _, err := sr.DB.Collection(sr.Config.Database.ShortenersCollection).UpdateOne(ctx, + bson.D{{Key: "short_url", Value: req.ShortURL}}, bson.M{ + "$set": bson.D{{Key: "visited", Value: lastVisitedCount + 1}, {Key: "updated_at", Value: time.Now()}}, + }) + if err != nil { + sr.Logger.Error("ShortRepositoryImpl.UpdateVisitorByShortURL UpdateOne ERROR, ", err) + return err + } + + return nil +} diff --git a/shortener/internal/v1/service/short.go b/shortener/internal/v1/service/short.go index 6d22df5..accf58f 100644 --- a/shortener/internal/v1/service/short.go +++ b/shortener/internal/v1/service/short.go @@ -2,10 +2,13 @@ package service import ( "context" + "net/url" + "time" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/config" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/model" "github.com/PickHD/singkatin-revamp/shortener/internal/v1/repository" + "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) @@ -14,6 +17,8 @@ type ( ShortService interface { GetListShortenerByUserID(ctx context.Context, userID string) ([]model.Short, error) CreateShort(ctx context.Context, req *model.CreateShortRequest) error + ClickShort(shortURL string) (*model.ClickShortResponse, error) + UpdateVisitorShort(ctx context.Context, req *model.UpdateVisitorRequest) error } // ShortServiceImpl is an app short struct that consists of all the dependencies needed for short repository @@ -45,9 +50,94 @@ func (ss *ShortServiceImpl) GetListShortenerByUserID(ctx context.Context, userID } func (ss *ShortServiceImpl) CreateShort(ctx context.Context, req *model.CreateShortRequest) error { + err := ss.validateCreateShort(req) + if err != nil { + return err + } + return ss.ShortRepo.Create(ctx, &model.Short{ FullURL: req.FullURL, ShortURL: req.ShortURL, UserID: req.UserID, }) } + +func (ss *ShortServiceImpl) ClickShort(shortURL string) (*model.ClickShortResponse, error) { + var ( + redisTTLDuration = time.Minute * time.Duration(ss.Config.Redis.TTL) + ) + + req := &model.UpdateVisitorRequest{ShortURL: shortURL} + + err := ss.validateClickShort(req) + if err != nil { + return nil, err + } + + cachedFullURL, err := ss.ShortRepo.GetFullURLByKey(ss.Context, req.ShortURL) + if err != nil { + if err == redis.Nil { + ss.Logger.Info("get data from default databases....") + + data, err := ss.ShortRepo.GetByShortURL(ss.Context, req.ShortURL) + if err != nil { + return nil, err + } + + err = ss.ShortRepo.SetFullURLByKey(ss.Context, req.ShortURL, data.FullURL, redisTTLDuration) + if err != nil { + return nil, err + } + + err = ss.ShortRepo.PublishUpdateVisitorCount(ss.Context, req) + if err != nil { + return nil, err + } + + return &model.ClickShortResponse{FullURL: data.FullURL}, nil + } + } + + ss.Logger.Info("get data from caching....") + + err = ss.ShortRepo.PublishUpdateVisitorCount(ss.Context, req) + if err != nil { + return nil, err + } + + return &model.ClickShortResponse{FullURL: cachedFullURL}, nil +} + +func (ss *ShortServiceImpl) UpdateVisitorShort(ctx context.Context, req *model.UpdateVisitorRequest) error { + data, err := ss.ShortRepo.GetByShortURL(ss.Context, req.ShortURL) + if err != nil { + return err + } + + err = ss.ShortRepo.UpdateVisitorByShortURL(ctx, req, data.Visited) + if err != nil { + return err + } + + return nil +} + +func (ss *ShortServiceImpl) validateCreateShort(req *model.CreateShortRequest) error { + if _, err := url.ParseRequestURI(req.FullURL); err != nil { + return model.NewError(model.Validation, err.Error()) + } + + return nil +} + +func (ss *ShortServiceImpl) validateClickShort(req *model.UpdateVisitorRequest) error { + if req.ShortURL == "" { + return model.NewError(model.Validation, "short URL cannot be empty") + } + + if len(req.ShortURL) != 8 { + return model.NewError(model.Validation, "short URL length must be 8") + } + + return nil +}