Skip to content

Commit

Permalink
URL Create limit added (#91)
Browse files Browse the repository at this point in the history
* initial checkin

* count added

* migration added

* I don't know this

* Update controllers/url.go

Co-authored-by: Yash Raj <[email protected]>

* test db updateds

* Update controllers/url.go

Co-authored-by: Yash Raj <[email protected]>

* typo fix for IsDeleted

* syntex fix

* chore: fix naming deleted_at

* chore: update error response

* minor chnages

* count is 50 now

---------

Co-authored-by: Yash Raj <[email protected]>
Co-authored-by: Prakash <[email protected]>
  • Loading branch information
3 people authored Jun 14, 2024
1 parent a50e792 commit 6c48a8f
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
BEGIN;
--bun:split

ALTER TABLE tiny_url DROP COLUMN is_deleted;
--bun:split

ALTER TABLE tiny_url DROP COLUMN deleted_at;

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
BEGIN;

--bun:split

ALTER TABLE tiny_url ADD is_deleted bool null DEFAULT FALSE;

--bun:split

ALTER TABLE tiny_url ADD deleted_at timestamp null;

COMMIT;
217 changes: 122 additions & 95 deletions controllers/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,103 +13,113 @@ import (
)

func CreateTinyURL(ctx *gin.Context, db *bun.DB) {
var body models.Tinyurl

if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Invalid JSON format: " + err.Error(),
})
return
}

if body.OriginalUrl == "" {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Original URL is required",
})
return
}

var existingOriginalURL models.Tinyurl
if err := db.NewSelect().Model(&existingOriginalURL).Where("original_url = ?", body.OriginalUrl).Limit(1).Scan(ctx); err == nil {
ctx.JSON(http.StatusOK, dtos.URLCreationResponse{
Message: "Tiny URL already exists for the original URL",
ShortURL: existingOriginalURL.ShortUrl,
})
return
}

if body.ShortUrl != "" {
if len(body.ShortUrl) < 5 {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Custom short URL must be at least 5 characters long",
})
return
}

var existingURL models.Tinyurl
if err := db.NewSelect().Model(&existingURL).Where("short_url = ?", body.ShortUrl).Limit(1).Scan(ctx); err == nil {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Custom short URL already exists",
})
return
}
} else {
generatedShortURL := utils.GenerateMD5Hash(body.OriginalUrl)
var existingURL models.Tinyurl
if err := db.NewSelect().Model(&existingURL).Where("short_url = ?", generatedShortURL).Limit(1).Scan(ctx); err != nil {
body.ShortUrl = generatedShortURL
}
}

body.CreatedAt = time.Now().UTC()
if _, err := db.NewInsert().Model(&body).Exec(ctx); err != nil {
ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{
Message: "Failed to insert into the database: " + err.Error(),
})
return
}

ctx.JSON(http.StatusOK, dtos.URLCreationResponse{
Message: "Tiny URL created successfully",
ShortURL: body.ShortUrl,
})
var body models.Tinyurl

if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Invalid Request.",
})
return
}

if body.OriginalUrl == "" {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "URL is required",
})
return
}

var existingOriginalURL models.Tinyurl
if err := db.NewSelect().Model(&existingOriginalURL).Where("original_url = ?", body.OriginalUrl).Limit(1).Scan(ctx); err == nil {
ctx.JSON(http.StatusOK, dtos.URLCreationResponse{
Message: "Shortened URL already exists",
ShortURL: existingOriginalURL.ShortUrl,
})
return
}

if body.ShortUrl != "" {
if len(body.ShortUrl) < 5 {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Custom short URL must be at least 5 characters long",
})
return
}

var existingURL models.Tinyurl
if err := db.NewSelect().Model(&existingURL).Where("short_url = ?", body.ShortUrl).Limit(1).Scan(ctx); err == nil {
ctx.JSON(http.StatusBadRequest, dtos.URLCreationResponse{
Message: "Custom short URL already exists",
})
return
}
} else {
generatedShortURL := utils.GenerateMD5Hash(body.OriginalUrl)
var existingURL models.Tinyurl
if err := db.NewSelect().Model(&existingURL).Where("short_url = ?", generatedShortURL).Limit(1).Scan(ctx); err != nil {
body.ShortUrl = generatedShortURL
}
}
count, _ := db.NewSelect().Model(&models.Tinyurl{}).Where("user_id = ?", body.UserID).Where("is_deleted=?", false).Count(ctx)

body.CreatedAt = time.Now().UTC()

if count >= 50 {

ctx.JSON(http.StatusForbidden, dtos.URLCreationResponse{
Message: "Url Limit Reached, Please Delete to Create New !",
})
return
}

if _, err := db.NewInsert().Model(&body).Exec(ctx); err != nil {
ctx.JSON(http.StatusInternalServerError, dtos.URLCreationResponse{
Message: "OOPS!!, Unable to process your request at this moment, Please try after sometime. ",
})
return
}

ctx.JSON(http.StatusOK, dtos.URLCreationResponse{
Message: "Tiny URL created successfully",
ShortURL: body.ShortUrl,
})
}

func RedirectShortURL(ctx *gin.Context, db *bun.DB) {
shortURL := ctx.Param("shortURL")

var tinyURL models.Tinyurl
err := db.NewSelect().
Model(&tinyURL).
Where("short_url = ?", shortURL).
Scan(ctx, &tinyURL)
if err != nil {
ctx.JSON(http.StatusNotFound, dtos.URLDetailsResponse{
Message: "Short URL not found",
})
return
}

if !strings.HasPrefix(tinyURL.OriginalUrl, "http://") && !strings.HasPrefix(tinyURL.OriginalUrl, "https://") {
tinyURL.OriginalUrl = "http://" + tinyURL.OriginalUrl
}

tinyURL.AccessCount++
tinyURL.LastAccessedAt = time.Now().UTC()

_, err = db.NewUpdate().
Model(&tinyURL).
Column("access_count", "last_accessed_at").
WherePK().
Exec(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "Failed to update access count and timestamp",
})
return
}

ctx.Redirect(http.StatusMovedPermanently, tinyURL.OriginalUrl)
shortURL := ctx.Param("shortURL")

var tinyURL models.Tinyurl
err := db.NewSelect().
Model(&tinyURL).
Where("short_url = ?", shortURL).
Scan(ctx, &tinyURL)
if err != nil {
ctx.JSON(http.StatusNotFound, dtos.URLDetailsResponse{
Message: "Short URL not found",
})
return
}

if !strings.HasPrefix(tinyURL.OriginalUrl, "http://") && !strings.HasPrefix(tinyURL.OriginalUrl, "https://") {
tinyURL.OriginalUrl = "http://" + tinyURL.OriginalUrl
}

tinyURL.AccessCount++
tinyURL.LastAccessedAt = time.Now().UTC()

_, err = db.NewUpdate().
Model(&tinyURL).
Column("access_count", "last_accessed_at").
WherePK().
Exec(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"message": "Failed to update access count and timestamp",
})
return
}

ctx.Redirect(http.StatusMovedPermanently, tinyURL.OriginalUrl)
}

func GetAllURLs(ctx *gin.Context, db *bun.DB) {
Expand All @@ -132,7 +142,7 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) {

err := db.NewSelect().
Model(&tinyURLs).
Where("user_id = ?", userID).
Where("user_id = ?", userID).Where("is_deleted=?", false).
Order("created_at DESC").
Scan(ctx, &tinyURLs)

Expand All @@ -148,6 +158,8 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) {
OriginalURL: tinyURL.OriginalUrl,
ShortURL: tinyURL.ShortUrl,
CreatedAt: tinyURL.CreatedAt,
ID: tinyURL.ID,
UserID: tinyURL.UserID,
})
}

Expand All @@ -156,6 +168,21 @@ func GetAllURLs(ctx *gin.Context, db *bun.DB) {
URLs: urlDetails,
})
}
func DeleteURL(ctx *gin.Context, db *bun.DB) {
id, _ := ctx.Params.Get("id")
_, err := db.NewUpdate().Model(&models.Tinyurl{}).Set("is_deleted=?", true).Set("deleted_at=?", time.Now().UTC()).Where("id = ?", id).Exec(ctx)

if err != nil {
ctx.JSON(http.StatusNotFound, dtos.UserURLsResponse{
Message: "No URLs found",
})
return
}

ctx.JSON(http.StatusOK, dtos.UserURLsResponse{
Message: "Url deleted",
})
}

func GetURLDetails(ctx *gin.Context, db *bun.DB) {
shortURL := ctx.Param("shortURL")
Expand Down
3 changes: 2 additions & 1 deletion migrations/20231007120000_create_tiny_url.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ CREATE TABLE tiny_url (
created_at timestamp DEFAULT (NOW() AT TIME ZONE 'UTC'),
created_by text NOT NULL,
access_count bigint DEFAULT 0,
last_accessed_at timestamp DEFAULT (NOW() AT TIME ZONE 'UTC')
last_accessed_at timestamp DEFAULT (NOW() AT TIME ZONE 'UTC'),
is_deleted bit null DEFAULT 0
);

COMMIT;
6 changes: 4 additions & 2 deletions models/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ type Tinyurl struct {
OriginalUrl string `bun:"original_url,notnull" json:"originalUrl"`
ShortUrl string `bun:"short_url,unique,notnull" json:"shortUrl"`
Comment string `bun:"comment" json:"comment"`
UserID int64 `bun:"user_id"`
User *User `bun:"rel:belongs-to,join:user_id=id"`
UserID int64 `bun:"user_id"`
User *User `bun:"rel:belongs-to,join:user_id=id"`
ExpiredAt time.Time `bun:"expired_at,notnull" json:"expiredAt"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp" json:"createdAt"`
CreatedBy string `bun:"created_by,notnull" json:"createdBy"`
AccessCount int64 `bun:"access_count,default:0" json:"accessCount"`
LastAccessedAt time.Time `bun:"last_accessed_at,nullzero" json:"lastAccessedAt"`
IsDeleted bool `bun:"is_deleted,null" json:"isDeleted"`
DeletedAt time.Time `bun:"deleted_at,nullzero,null," json:"deletedAt"`
}
3 changes: 3 additions & 0 deletions routes/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ func TinyURLRoutes(rg *gin.RouterGroup, db *bun.DB) {
redirect.GET("/:shortURL", func(ctx *gin.Context) {
controller.RedirectShortURL(ctx, db)
})
urls.DELETE("/:id", func(ctx *gin.Context) {
controller.DeleteURL(ctx, db)
})
}
2 changes: 1 addition & 1 deletion tests/integration/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func (suite *AppTestSuite) TestCreateTinyURLExistingOriginalURL() {

assert.Equal(suite.T(), http.StatusOK, w.Code, "Expected status code to be 200 for existing original URL")

expectedResponse := `{"message":"Tiny URL already exists for the original URL","shortUrl":"short","createdAt":"0001-01-01T00:00:00Z"}`
expectedResponse := `{"message":"Shortened URL already exists", "shortUrl":"short","shortUrl":"short","createdAt":"0001-01-01T00:00:00Z"}`
assert.JSONEq(suite.T(), expectedResponse, w.Body.String(), "Response body does not match expected JSON")
}

Expand Down
4 changes: 3 additions & 1 deletion tests/test-data/init-db.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ CREATE TABLE IF NOT EXISTS tiny_url (
created_at timestamp WITH TIME ZONE DEFAULT (NOW() AT TIME ZONE 'UTC'),
created_by text NOT NULL,
access_count bigint DEFAULT 0,
last_accessed_at timestamp DEFAULT (NOW() AT TIME ZONE 'UTC')
last_accessed_at timestamp DEFAULT (NOW() AT TIME ZONE 'UTC'),
is_deleted bool null default FALSE,
deleted_at timestamp WITH TIME ZONE NULL
);

-- Insert data into the tiny_url table
Expand Down

0 comments on commit 6c48a8f

Please sign in to comment.