Skip to content

Commit

Permalink
feat (SRS-06) : implement API click shorteners
Browse files Browse the repository at this point in the history
  • Loading branch information
PickHD committed May 8, 2023
1 parent 68ec32d commit 47681d0
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 20 deletions.
2 changes: 1 addition & 1 deletion shortener/internal/v1/application/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion shortener/internal/v1/application/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion shortener/internal/v1/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
}
}
Expand Down
48 changes: 48 additions & 0 deletions shortener/internal/v1/controller/short.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
2 changes: 2 additions & 0 deletions shortener/internal/v1/infrastructure/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
5 changes: 5 additions & 0 deletions shortener/internal/v1/model/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package model

const (
KeyShortURL = "short_url:%s"
)
26 changes: 20 additions & 6 deletions shortener/internal/v1/model/short.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
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 {
UserID string `json:"user_id"`
FullURL string `json:"full_url"`
ShortURL string `json:"short_url"`
}

ClickShortResponse struct {
FullURL string `json:"full_url"`
}

UpdateVisitorRequest struct {
ShortURL string `json:"short_url"`
}
)
124 changes: 113 additions & 11 deletions shortener/internal/v1/repository/short.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,61 @@ 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 (
// ShortRepository is an interface that has all the function to be implemented inside short repository
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
Expand All @@ -66,11 +83,96 @@ 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
}

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

0 comments on commit 47681d0

Please sign in to comment.