Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL Create limit added #91

Merged
merged 16 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading