From 245ddbc137ab7aa2b54cc1e2beab3ba2fb545a21 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Wed, 18 Jan 2023 20:55:51 +0100 Subject: [PATCH 1/4] Database related operation files reorganization --- internal/database/database.go | 77 ----------------------------------- internal/database/delete.go | 19 +++++++++ internal/database/edit.go | 29 +++++++++++++ internal/database/get.go | 35 ++++++++++++++++ internal/database/push.go | 15 +++++++ 5 files changed, 98 insertions(+), 77 deletions(-) create mode 100644 internal/database/delete.go create mode 100644 internal/database/edit.go create mode 100644 internal/database/get.go create mode 100644 internal/database/push.go diff --git a/internal/database/database.go b/internal/database/database.go index 0ad9853..1602d82 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -2,9 +2,7 @@ package database import ( "context" - "errors" - "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -14,11 +12,6 @@ type Location struct { Collection string } -type DocumentUpdate struct { - Filter gin.H `json:"filter"` - Update gin.H `json:"update"` -} - func GetNewClient() *mongo.Client { client, err := mongo.Connect( context.TODO(), @@ -28,73 +21,3 @@ func GetNewClient() *mongo.Client { } return client } - -func PushDocument(location Location, document interface{}) error { - client := GetNewClient() - collection := client.Database(location.Database).Collection(location.Collection) - defer client.Disconnect(context.TODO()) - - _, err := collection.InsertOne(context.TODO(), document) - if err != nil { - return err - } - return nil -} - -func GetDocuments(location Location, filter interface{}) ([][]byte, error) { - client := GetNewClient() - collection := client.Database(location.Database).Collection(location.Collection) - defer client.Disconnect(context.TODO()) - - var results [][]byte - - cursor, err := collection.Find(context.TODO(), filter) - if err != nil { - return results, errors.New("something is wrong with filter to find the document.") - } - for cursor.TryNext(context.TODO()) { - results = append(results, cursor.Current) - } - return results, nil -} - -func GetUniqueDocument(location Location, filter interface{}) ([]byte, error) { - client := GetNewClient() - collection := client.Database(location.Database).Collection(location.Collection) - defer client.Disconnect(context.TODO()) - - singleResult := collection.FindOne(context.TODO(), filter) - if singleResult.Err() != nil { - return nil, errors.New("no document was found, check the filter or the location.") - } - return singleResult.DecodeBytes() -} - -func DeleteDocument(location Location, filter interface{}) (int64, error) { - client := GetNewClient() - collection := client.Database(location.Database).Collection(location.Collection) - defer client.Disconnect(context.TODO()) - - result, err := collection.DeleteOne(context.TODO(), filter) - if err != nil { - return 0, errors.New("something is wrong with filter to delete the document.") - } - - return result.DeletedCount, nil -} - -func EditDocument(location Location, jsons DocumentUpdate) (int64, error) { - client := GetNewClient() - collection := client.Database(location.Database).Collection(location.Collection) - defer client.Disconnect(context.TODO()) - - result, err := collection.UpdateOne(context.TODO(), jsons.Filter, gin.H{"$set": jsons.Update}) - if err != nil { - return 0, err - } - if result.MatchedCount == 0 { - return 0, errors.New("cannot find document matching filter.") - } - - return result.ModifiedCount, nil -} diff --git a/internal/database/delete.go b/internal/database/delete.go new file mode 100644 index 0000000..0669b20 --- /dev/null +++ b/internal/database/delete.go @@ -0,0 +1,19 @@ +package database + +import ( + "context" + "errors" +) + +func DeleteDocument(location Location, filter interface{}) (int64, error) { + client := GetNewClient() + collection := client.Database(location.Database).Collection(location.Collection) + defer client.Disconnect(context.TODO()) + + result, err := collection.DeleteOne(context.TODO(), filter) + if err != nil { + return 0, errors.New("something is wrong with filter to delete the document.") + } + + return result.DeletedCount, nil +} diff --git a/internal/database/edit.go b/internal/database/edit.go new file mode 100644 index 0000000..11ae8db --- /dev/null +++ b/internal/database/edit.go @@ -0,0 +1,29 @@ +package database + +import ( + "context" + "errors" + + "github.com/gin-gonic/gin" +) + +type DocumentUpdate struct { + Filter gin.H `json:"filter"` + Update gin.H `json:"update"` +} + +func EditDocument(location Location, jsons DocumentUpdate) (int64, error) { + client := GetNewClient() + collection := client.Database(location.Database).Collection(location.Collection) + defer client.Disconnect(context.TODO()) + + result, err := collection.UpdateOne(context.TODO(), jsons.Filter, gin.H{"$set": jsons.Update}) + if err != nil { + return 0, err + } + if result.MatchedCount == 0 { + return 0, errors.New("cannot find document matching filter.") + } + + return result.ModifiedCount, nil +} diff --git a/internal/database/get.go b/internal/database/get.go new file mode 100644 index 0000000..e53e806 --- /dev/null +++ b/internal/database/get.go @@ -0,0 +1,35 @@ +package database + +import ( + "context" + "errors" +) + +func GetDocuments(location Location, filter interface{}) ([][]byte, error) { + client := GetNewClient() + collection := client.Database(location.Database).Collection(location.Collection) + defer client.Disconnect(context.TODO()) + + var results [][]byte + + cursor, err := collection.Find(context.TODO(), filter) + if err != nil { + return results, errors.New("something is wrong with filter to find the document.") + } + for cursor.TryNext(context.TODO()) { + results = append(results, cursor.Current) + } + return results, nil +} + +func GetUniqueDocument(location Location, filter interface{}) ([]byte, error) { + client := GetNewClient() + collection := client.Database(location.Database).Collection(location.Collection) + defer client.Disconnect(context.TODO()) + + singleResult := collection.FindOne(context.TODO(), filter) + if singleResult.Err() != nil { + return nil, errors.New("no document was found, check the filter or the location.") + } + return singleResult.DecodeBytes() +} diff --git a/internal/database/push.go b/internal/database/push.go new file mode 100644 index 0000000..370cd81 --- /dev/null +++ b/internal/database/push.go @@ -0,0 +1,15 @@ +package database + +import "context" + +func PushDocument(location Location, document interface{}) error { + client := GetNewClient() + collection := client.Database(location.Database).Collection(location.Collection) + defer client.Disconnect(context.TODO()) + + _, err := collection.InsertOne(context.TODO(), document) + if err != nil { + return err + } + return nil +} From c681b5389c89cc9844b5ed9ace77ce4dbe2f321a Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Wed, 18 Jan 2023 21:00:32 +0100 Subject: [PATCH 2/4] Added codes in API responses --- internal/api/messages.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/api/messages.go b/internal/api/messages.go index 301f7c7..539a819 100644 --- a/internal/api/messages.go +++ b/internal/api/messages.go @@ -7,13 +7,13 @@ import ( ) func SendBadRequest(c *gin.Context, message string) { - c.JSON(http.StatusBadRequest, gin.H{"message": message}) + c.JSON(http.StatusBadRequest, gin.H{"message": message, "code": 400}) } func SendOk(c *gin.Context, message string) { - c.JSON(http.StatusOK, gin.H{"message": message}) + c.JSON(http.StatusOK, gin.H{"message": message, "code": 200}) } func SendForbidden(c *gin.Context, message string) { - c.JSON(http.StatusForbidden, gin.H{"message": message}) + c.JSON(http.StatusForbidden, gin.H{"message": message, "code": 403}) } From 3376d88f5674fc6e645815428f1e79257d398b20 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Wed, 18 Jan 2023 23:33:34 +0100 Subject: [PATCH 3/4] Splat handler in their own files, added pagination --- README.md | 8 ++- cmd/main.go | 10 ++-- internal/articles/add.go | 39 +++++++++++++++ internal/articles/delete.go | 25 ++++++++++ internal/articles/edit.go | 29 +++++++++++ internal/articles/get.go | 80 ++++++++++++++++++++++++++++++ internal/articles/handlers.go | 93 ----------------------------------- 7 files changed, 185 insertions(+), 99 deletions(-) create mode 100644 internal/articles/add.go create mode 100644 internal/articles/delete.go create mode 100644 internal/articles/edit.go create mode 100644 internal/articles/get.go delete mode 100644 internal/articles/handlers.go diff --git a/README.md b/README.md index 620fb36..3d22f18 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ The features are not complete, not tested and subject to many changes. ## Setup +Don't forget to setup the [Environment variables](#environment-variables)! + ### Use with Docker To **run** the app, run the command @@ -18,7 +20,7 @@ docker-compose up TODO -## Environment variables +### Environment variables - ./env - APP_API_ADDRESS: `http://example.com` @@ -29,6 +31,10 @@ TODO - APP_FRONT_PORT: `1234` - APP_JWT_SECRET: `secret` +## API Usage + +TODO + ## Demo TODO diff --git a/cmd/main.go b/cmd/main.go index eb38be8..7719433 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -63,11 +63,11 @@ func initArticlesRoutes(r *gin.RouterGroup) { articlesRouter := r.Group("/articles") articlesRouter.Use(corsMiddleware, api.AuthMiddleware.MiddlewareFunc()) - articlesRouter.GET("/", articles.GetArticleHandler) - articlesRouter.GET("/:id", articles.GetArticleHandler) - articlesRouter.POST("/:id", articles.AddArticleHandler) - articlesRouter.PUT("/:id", articles.EditArticleHandler) - articlesRouter.DELETE("/:id", articles.DeleteArticleHandler) + articlesRouter.GET("/", articles.Get) + articlesRouter.GET("/:id", articles.Get) + articlesRouter.POST("/:id", articles.Add) + articlesRouter.PUT("/:id", articles.Edit) + articlesRouter.DELETE("/:id", articles.Delete) } func initGin() { diff --git a/internal/articles/add.go b/internal/articles/add.go new file mode 100644 index 0000000..a564fb8 --- /dev/null +++ b/internal/articles/add.go @@ -0,0 +1,39 @@ +package articles + +import ( + "fmt" + + "github.com/Floriansylvain/GohCMS/internal/api" + "github.com/Floriansylvain/GohCMS/internal/database" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" +) + +func Add(c *gin.Context) { + var article Article + article.TitleID = c.Params.ByName("id") + if c.BindJSON(&article) != nil { + api.SendBadRequest(c, "Could not correctly parse the article.") + return + } + + document, err := bson.Marshal(article) + if err != nil { + api.SendBadRequest(c, "Could not correctly marshal the article.") + return + } + + documents, _ := database.GetDocuments(articlesLocation, map[string]any{}) + if IsArticleIdAlreadyUsed(article.TitleID, documents) { + api.SendBadRequest(c, fmt.Sprintf("Article ID '%v' already used.", article.TitleID)) + return + } + + err = database.PushDocument(articlesLocation, document) + if err != nil { + api.SendBadRequest(c, fmt.Sprintf(`Could not insert document(s) into DB: %v`, err.Error())) + return + } + + api.SendOk(c, "Article successfully added!") +} diff --git a/internal/articles/delete.go b/internal/articles/delete.go new file mode 100644 index 0000000..835e209 --- /dev/null +++ b/internal/articles/delete.go @@ -0,0 +1,25 @@ +package articles + +import ( + "fmt" + + "github.com/Floriansylvain/GohCMS/internal/api" + "github.com/Floriansylvain/GohCMS/internal/database" + "github.com/gin-gonic/gin" +) + +func Delete(c *gin.Context) { + id := c.Params.ByName("id") + + deleteCount, err := database.DeleteDocument(articlesLocation, map[string]any{"titleID": id}) + if err != nil { + api.SendBadRequest(c, fmt.Sprintf(`Could not delete document(s) from DB: %v`, err.Error())) + return + } + + if deleteCount != 0 { + api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully deleted!", deleteCount)) + } else { + api.SendOk(c, "No articles were deleted.") + } +} diff --git a/internal/articles/edit.go b/internal/articles/edit.go new file mode 100644 index 0000000..f08138a --- /dev/null +++ b/internal/articles/edit.go @@ -0,0 +1,29 @@ +package articles + +import ( + "fmt" + + "github.com/Floriansylvain/GohCMS/internal/api" + "github.com/Floriansylvain/GohCMS/internal/database" + "github.com/gin-gonic/gin" +) + +func Edit(c *gin.Context) { + id := c.Params.ByName("id") + + var articleUpdate database.DocumentUpdate + articleUpdate.Filter = map[string]any{"titleID": id} + c.BindJSON(&articleUpdate.Update) + + editCount, err := database.EditDocument(articlesLocation, articleUpdate) + if err != nil { + api.SendBadRequest(c, fmt.Sprintf(`Could not edit document(s) from DB: %v`, err.Error())) + return + } + + if editCount != 0 { + api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully edited!", editCount)) + } else { + api.SendOk(c, "No articles were edited.") + } +} diff --git a/internal/articles/get.go b/internal/articles/get.go new file mode 100644 index 0000000..1f6f531 --- /dev/null +++ b/internal/articles/get.go @@ -0,0 +1,80 @@ +package articles + +import ( + "fmt" + "net/http" + "os" + "strconv" + + "github.com/Floriansylvain/GohCMS/internal/api" + "github.com/Floriansylvain/GohCMS/internal/database" + "github.com/gin-gonic/gin" +) + +func getApiFullUrl() string { + return fmt.Sprintf("%v%v", os.Getenv("APP_API_ADDRESS"), os.Getenv("APP_BASE_API_PATH")) +} + +func getArticleSkipTakeFullUrl(skip uint64, take uint64) string { + return fmt.Sprintf("%v/articles?skip=%v&take=%v", getApiFullUrl(), skip+take, take) +} + +func getBuiltGetResponse(articles []Article, skip uint64, take uint64) map[string]any { + slicedArticles := articles[skip : skip+take] + return map[string]any{ + "content": slicedArticles, + "total": len(slicedArticles), + "pagination": map[string]any{ + "skip": skip, + "take": take, + "links": map[string]any{ + "next": getArticleSkipTakeFullUrl(skip+take, take), + "previous": getArticleSkipTakeFullUrl(skip-take, take), + }, + }, + } +} + +func parseUintQueryParam(c *gin.Context, param string) (uint64, error) { + value, err := strconv.ParseUint(c.Query(param), 10, 0) + if err != nil { + c.JSON(http.StatusBadRequest, fmt.Sprintf("%s query parameter must be a positive number.", param)) + } + return value, err +} + +func getArticleIdFilter(titleID string) map[string]any { + filter := map[string]any{} + if titleID != "" { + filter["titleID"] = titleID + } + return filter +} + +func Get(c *gin.Context) { + titleID := c.Params.ByName("id") + + skip, skipErr := parseUintQueryParam(c, "skip") + take, takeErr := parseUintQueryParam(c, "take") + if skipErr != nil || takeErr != nil { + return + } + + articles, err := database.GetDocuments(articlesLocation, getArticleIdFilter(titleID)) + if err != nil { + api.SendBadRequest(c, fmt.Sprintf("The ID '%v' doesn't match any article.", titleID)) + return + } + + articlesArray := ParseArticlesFromBytesToArray(articles) + articlesArrayLength := uint64(len(articlesArray)) + if skip >= articlesArrayLength || take == 0 { + c.JSON(http.StatusOK, map[string]any{ + "content": []string{}, + "total": "0", + }) + return + } + + c.JSON(http.StatusOK, getBuiltGetResponse(articlesArray, skip, take)) +} diff --git a/internal/articles/handlers.go b/internal/articles/handlers.go deleted file mode 100644 index 55856e4..0000000 --- a/internal/articles/handlers.go +++ /dev/null @@ -1,93 +0,0 @@ -package articles - -import ( - "fmt" - "net/http" - - "github.com/Floriansylvain/GohCMS/internal/api" - "github.com/Floriansylvain/GohCMS/internal/database" - "github.com/gin-gonic/gin" - "go.mongodb.org/mongo-driver/bson" -) - -func GetArticleHandler(c *gin.Context) { - articleID := c.Params.ByName("id") - - filter := gin.H{} - if articleID != "" { - filter["titleID"] = articleID - } - - articles, err := database.GetDocuments(articlesLocation, filter) - if err != nil { - api.SendBadRequest(c, fmt.Sprintf("The ID '%v' doesn't match any article.", articleID)) - return - } - - c.JSON(http.StatusOK, ParseArticlesFromBytesToArray(articles)) -} - -func AddArticleHandler(c *gin.Context) { - var article Article - article.TitleID = c.Params.ByName("id") - if c.BindJSON(&article) != nil { - api.SendBadRequest(c, "Could not correctly parse the article.") - return - } - - document, err := bson.Marshal(article) - if err != nil { - api.SendBadRequest(c, "Could not correctly marshal the article.") - return - } - - documents, _ := database.GetDocuments(articlesLocation, gin.H{}) - if IsArticleIdAlreadyUsed(article.TitleID, documents) { - api.SendBadRequest(c, fmt.Sprintf("Article ID '%v' already used.", article.TitleID)) - return - } - - err = database.PushDocument(articlesLocation, document) - if err != nil { - api.SendBadRequest(c, fmt.Sprintf(`Could not insert document(s) into DB: %v`, err.Error())) - return - } - - api.SendOk(c, "Article successfully added!") -} - -func DeleteArticleHandler(c *gin.Context) { - id := c.Params.ByName("id") - - deleteCount, err := database.DeleteDocument(articlesLocation, gin.H{"titleID": id}) - if err != nil { - api.SendBadRequest(c, fmt.Sprintf(`Could not delete document(s) from DB: %v`, err.Error())) - return - } - - if deleteCount != 0 { - api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully deleted!", deleteCount)) - } else { - api.SendOk(c, "No articles were deleted.") - } -} - -func EditArticleHandler(c *gin.Context) { - id := c.Params.ByName("id") - - var articleUpdate database.DocumentUpdate - articleUpdate.Filter = gin.H{"titleID": id} - c.BindJSON(&articleUpdate.Update) - - editCount, err := database.EditDocument(articlesLocation, articleUpdate) - if err != nil { - api.SendBadRequest(c, fmt.Sprintf(`Could not edit document(s) from DB: %v`, err.Error())) - return - } - - if editCount != 0 { - api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully edited!", editCount)) - } else { - api.SendOk(c, "No articles were edited.") - } -} From 1ffc999e1ae39f0b853a4f6ac75ffb19d9cdec02 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Thu, 19 Jan 2023 00:19:52 +0100 Subject: [PATCH 4/4] Solved several pagination problems --- internal/articles/get.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/articles/get.go b/internal/articles/get.go index 1f6f531..ad7d74e 100644 --- a/internal/articles/get.go +++ b/internal/articles/get.go @@ -16,11 +16,20 @@ func getApiFullUrl() string { } func getArticleSkipTakeFullUrl(skip uint64, take uint64) string { - return fmt.Sprintf("%v/articles?skip=%v&take=%v", getApiFullUrl(), skip+take, take) + return fmt.Sprintf("%varticles?skip=%v&take=%v", getApiFullUrl(), skip+take, take) } func getBuiltGetResponse(articles []Article, skip uint64, take uint64) map[string]any { - slicedArticles := articles[skip : skip+take] + articlesCap := uint64(len(articles)) + + var normTake uint64 + if skip+take > articlesCap { + normTake = articlesCap + } else { + normTake = skip + take + } + + slicedArticles := articles[skip:normTake] return map[string]any{ "content": slicedArticles, "total": len(slicedArticles), @@ -35,11 +44,11 @@ func getBuiltGetResponse(articles []Article, skip uint64, take uint64) map[strin } } -func parseUintQueryParam(c *gin.Context, param string) (uint64, error) { - value, err := strconv.ParseUint(c.Query(param), 10, 0) - if err != nil { - c.JSON(http.StatusBadRequest, fmt.Sprintf("%s query parameter must be a positive number.", param)) +func parseUintQueryParam(c *gin.Context, param string, defaultValue uint64) (uint64, error) { + if c.Query(param) == "" { + return defaultValue, nil } + value, err := strconv.ParseUint(c.Query(param), 10, 0) return value, err } @@ -54,9 +63,10 @@ func getArticleIdFilter(titleID string) map[string]any { func Get(c *gin.Context) { titleID := c.Params.ByName("id") - skip, skipErr := parseUintQueryParam(c, "skip") - take, takeErr := parseUintQueryParam(c, "take") + skip, skipErr := parseUintQueryParam(c, "skip", 0) + take, takeErr := parseUintQueryParam(c, "take", 10) if skipErr != nil || takeErr != nil { + api.SendBadRequest(c, "Take and Skip query parameters must be positive numbers.") return }