From 39392115a06de797d7fb8c6047afc1caa36003dd Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Thu, 11 Jan 2024 14:00:01 +0530 Subject: [PATCH] feat(api): add pagination capabilities with middleware Signed-off-by: Gaurav Mishra --- cmd/laas/docs/docs.go | 56 +++++++- cmd/laas/docs/swagger.json | 56 +++++++- cmd/laas/docs/swagger.yaml | 40 +++++- pkg/api/api.go | 8 +- pkg/api/audit.go | 15 ++- pkg/api/licenses.go | 23 +++- pkg/api/obligations.go | 18 ++- pkg/auth/auth.go | 95 -------------- pkg/middleware/middleware.go | 248 +++++++++++++++++++++++++++++++++++ pkg/models/types.go | 49 +++++-- pkg/utils/util.go | 37 ++++++ 11 files changed, 510 insertions(+), 135 deletions(-) create mode 100644 pkg/middleware/middleware.go diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 0750b43..3b657d5 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -42,6 +42,20 @@ const docTemplate = `{ ], "summary": "Get audit records", "operationId": "GetAllAudit", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { "description": "Audit records", @@ -313,6 +327,18 @@ const docTemplate = `{ "description": "Copyleft flag status of license", "name": "copyleft", "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit of responses per page", + "name": "limit", + "in": "query" } ], "responses": { @@ -756,6 +782,18 @@ const docTemplate = `{ "name": "active", "in": "query", "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page", + "name": "limit", + "in": "query" } ], "responses": { @@ -1718,15 +1756,27 @@ const docTemplate = `{ "models.PaginationMeta": { "type": "object", "properties": { - "page": { + "limit": { "type": "integer", - "example": 1 + "example": 10 + }, + "next": { + "type": "string", + "example": "/api/v1/licenses?limit=10\u0026page=11" }, - "per_page": { + "page": { "type": "integer", "example": 10 }, + "previous": { + "type": "string", + "example": "/api/v1/licenses?limit=10\u0026page=9" + }, "resource_count": { + "type": "integer", + "example": 200 + }, + "total_pages": { "type": "integer", "example": 20 } diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index e4932df..c9c32f5 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -36,6 +36,20 @@ ], "summary": "Get audit records", "operationId": "GetAllAudit", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { "description": "Audit records", @@ -307,6 +321,18 @@ "description": "Copyleft flag status of license", "name": "copyleft", "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit of responses per page", + "name": "limit", + "in": "query" } ], "responses": { @@ -750,6 +776,18 @@ "name": "active", "in": "query", "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Number of records per page", + "name": "limit", + "in": "query" } ], "responses": { @@ -1712,15 +1750,27 @@ "models.PaginationMeta": { "type": "object", "properties": { - "page": { + "limit": { "type": "integer", - "example": 1 + "example": 10 + }, + "next": { + "type": "string", + "example": "/api/v1/licenses?limit=10\u0026page=11" }, - "per_page": { + "page": { "type": "integer", "example": 10 }, + "previous": { + "type": "string", + "example": "/api/v1/licenses?limit=10\u0026page=9" + }, "resource_count": { + "type": "integer", + "example": 200 + }, + "total_pages": { "type": "integer", "example": 20 } diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index b692274..4e2f606 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -395,13 +395,22 @@ definitions: type: object models.PaginationMeta: properties: - page: - example: 1 + limit: + example: 10 type: integer - per_page: + next: + example: /api/v1/licenses?limit=10&page=11 + type: string + page: example: 10 type: integer + previous: + example: /api/v1/licenses?limit=10&page=9 + type: string resource_count: + example: 200 + type: integer + total_pages: example: 20 type: integer type: object @@ -535,6 +544,15 @@ paths: - application/json description: Get all audit records from the server operationId: GetAllAudit + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Number of records per page + in: query + name: limit + type: integer produces: - application/json responses: @@ -715,6 +733,14 @@ paths: in: query name: copyleft type: boolean + - description: Page number + in: query + name: page + type: integer + - description: Limit of responses per page + in: query + name: limit + type: integer produces: - application/json responses: @@ -1007,6 +1033,14 @@ paths: name: active required: true type: boolean + - description: Page number + in: query + name: page + type: integer + - description: Number of records per page + in: query + name: limit + type: integer produces: - application/json responses: diff --git a/pkg/api/api.go b/pkg/api/api.go index 890d318..889a809 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -16,6 +16,7 @@ import ( "github.com/fossology/LicenseDb/pkg/auth" "github.com/fossology/LicenseDb/pkg/db" + "github.com/fossology/LicenseDb/pkg/middleware" "github.com/fossology/LicenseDb/pkg/models" ) @@ -47,7 +48,10 @@ func Router() *gin.Engine { r.NoRoute(HandleInvalidUrl) // CORS middleware - r.Use(auth.CORSMiddleware()) + r.Use(middleware.CORSMiddleware()) + + // Pagination middleware + r.Use(middleware.PaginationMiddleware()) unAuthorizedv1 := r.Group("/api/v1") { @@ -81,7 +85,7 @@ func Router() *gin.Engine { } authorizedv1 := r.Group("/api/v1") - authorizedv1.Use(auth.AuthenticationMiddleware()) + authorizedv1.Use(middleware.AuthenticationMiddleware()) { licenses := authorizedv1.Group("/licenses") { diff --git a/pkg/api/audit.go b/pkg/api/audit.go index 1661eaa..3ff6c35 100644 --- a/pkg/api/audit.go +++ b/pkg/api/audit.go @@ -24,14 +24,19 @@ import ( // @Tags Audits // @Accept json // @Produce json -// @Success 200 {object} models.AuditResponse "Audit records" -// @Failure 404 {object} models.LicenseError "Not changelogs in DB" +// @Param page query int false "Page number" +// @Param limit query int false "Number of records per page" +// @Success 200 {object} models.AuditResponse "Audit records" +// @Failure 404 {object} models.LicenseError "Not changelogs in DB" // @Security ApiKeyAuth // @Router /audits [get] func GetAllAudit(c *gin.Context) { var audit []models.Audit + query := db.DB.Model(&models.Audit{}) - if err := db.DB.Find(&audit).Error; err != nil { + _ = utils.PreparePaginateResponse(c, query, &models.AuditResponse{}) + + if err := query.Find(&audit).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: "Change log not found", @@ -45,7 +50,7 @@ func GetAllAudit(c *gin.Context) { res := models.AuditResponse{ Data: audit, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: len(audit), }, } @@ -88,7 +93,7 @@ func GetAudit(c *gin.Context) { res := models.AuditResponse{ Data: []models.Audit{changelog}, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index 755cda9..727b16a 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -14,6 +14,7 @@ import ( "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" ) @@ -21,8 +22,11 @@ import ( func GetAllLicense(c *gin.Context) { var licenses []models.LicenseDB + query := db.DB.Model(&models.LicenseDB{}) - err := db.DB.Find(&licenses).Error + _ = utils.PreparePaginateResponse(c, query, &models.LicenseResponse{}) + + err := query.Find(&licenses).Error if err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, @@ -37,7 +41,7 @@ func GetAllLicense(c *gin.Context) { res := models.LicenseResponse{ Data: licenses, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: len(licenses), }, } @@ -62,6 +66,8 @@ func GetAllLicense(c *gin.Context) { // @Param osiapproved query bool false "OSI Approved flag status of license" // @Param fsffree query bool false "FSF Free flag status of license" // @Param copyleft query bool false "Copyleft flag status of license" +// @Param page query int false "Page number" +// @Param limit query int false "Limit of responses per page" // @Success 200 {object} models.LicenseResponse "Filtered licenses" // @Failure 400 {object} models.LicenseError "Invalid value" // @Router /licenses [get] @@ -169,12 +175,15 @@ func FilterLicense(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } + + _ = utils.PreparePaginateResponse(c, query, &models.LicenseResponse{}) + query.Find(&license) res := models.LicenseResponse{ Data: license, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: len(license), }, } @@ -218,7 +227,7 @@ func GetLicense(c *gin.Context) { res := models.LicenseResponse{ Data: []models.LicenseDB{license}, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } @@ -307,7 +316,7 @@ func CreateLicense(c *gin.Context) { res := models.LicenseResponse{ Data: []models.LicenseDB{license}, Status: http.StatusCreated, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } @@ -393,7 +402,7 @@ func UpdateLicense(c *gin.Context) { res := models.LicenseResponse{ Data: []models.LicenseDB{license}, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } @@ -645,7 +654,7 @@ func SearchInLicense(c *gin.Context) { res := models.LicenseResponse{ Data: license, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: len(license), }, } diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index 806ab3b..e01b94c 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -16,6 +16,7 @@ import ( "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" ) @@ -28,6 +29,8 @@ import ( // @Accept json // @Produce json // @Param active query bool true "Active obligation only" +// @Param page query int false "Page number" +// @Param limit query int false "Number of records per page" // @Success 200 {object} models.ObligationResponse // @Failure 404 {object} models.LicenseError "No obligations in DB" // @Router /obligations [get] @@ -50,7 +53,12 @@ func GetAllObligation(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } - if err = db.DB.Where("active = ?", parsedActive).Find(&obligations).Error; err != nil { + query := db.DB.Model(&models.Obligation{}) + query.Where("active = ?", parsedActive) + + _ = utils.PreparePaginateResponse(c, query, &models.ObligationResponse{}) + + if err = query.Find(&obligations).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: "Obligations not found", @@ -64,7 +72,7 @@ func GetAllObligation(c *gin.Context) { res := models.ObligationResponse{ Data: obligations, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: len(obligations), }, } @@ -102,7 +110,7 @@ func GetObligation(c *gin.Context) { res := models.ObligationResponse{ Data: []models.Obligation{obligation}, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } @@ -198,7 +206,7 @@ func CreateObligation(c *gin.Context) { res := models.ObligationResponse{ Data: []models.Obligation{obligation}, Status: http.StatusCreated, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } @@ -370,7 +378,7 @@ func UpdateObligation(c *gin.Context) { res := models.ObligationResponse{ Data: []models.Obligation{obligation}, Status: http.StatusOK, - Meta: models.PaginationMeta{ + Meta: &models.PaginationMeta{ ResourceCount: 1, }, } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index e7df985..0f9c13e 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -189,101 +189,6 @@ func GetUser(c *gin.Context) { c.JSON(http.StatusOK, res) } -// AuthenticationMiddleware is a middleware function for user authentication. -func AuthenticationMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - tokenString := c.GetHeader("Authorization") - - if tokenString == "" { - er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "Please check your credentials and try again", - Error: "no credentials were passed", - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - - c.JSON(http.StatusUnauthorized, er) - c.Abort() - return - } - - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(os.Getenv("API_SECRET")), nil - }) - - if err != nil { - er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "Please check your credentials and try again", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - - c.JSON(http.StatusUnauthorized, er) - c.Abort() - return - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { - er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "Invalid token", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - - c.JSON(http.StatusUnauthorized, er) - c.Abort() - return - } - - userId := int64(claims["id"].(float64)) - - var user models.User - result := db.DB.Where(models.User{Id: userId}).First(&user) - if result.Error != nil { - er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "User not found", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - - c.JSON(http.StatusUnauthorized, er) - c.Abort() - return - } - - c.Set("username", user.Username) - c.Next() - } -} - -// CORSMiddleware is a middleware function for CORS. -func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") - c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } -} - // Login user and get JWT tokens // // @Summary Login diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go new file mode 100644 index 0000000..0c5efdb --- /dev/null +++ b/pkg/middleware/middleware.go @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2023 Kavya Shukla +// SPDX-FileCopyrightText: 2024 Siemens AG +// SPDX-FileContributor: Gaurav Mishra +// +// SPDX-License-Identifier: GPL-2.0-only + +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "os" + "strconv" + "time" + + "github.com/fossology/LicenseDb/pkg/db" + "github.com/fossology/LicenseDb/pkg/models" + "github.com/fossology/LicenseDb/pkg/utils" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" +) + +// AuthenticationMiddleware is a middleware function for user authentication. +func AuthenticationMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + + if tokenString == "" { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "no credentials were passed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("API_SECRET")), nil + }) + + if err != nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Invalid token", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + userId := int64(claims["id"].(float64)) + + var user models.User + result := db.DB.Where(models.User{Id: userId}).First(&user) + if result.Error != nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "User not found", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + c.Set("username", user.Username) + c.Next() + } +} + +// CORSMiddleware is a middleware function for CORS. +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +// PaginationMiddleware handles pagination requests and responses. +func PaginationMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var page models.PaginationInput + parsedPage, err := strconv.ParseInt(c.Query("page"), 10, 64) + if err == nil { + page.Page = int(parsedPage) + } + parsedLimit, err := strconv.ParseInt(c.Query("limit"), 10, 64) + if err == nil { + page.Limit = int(parsedLimit) + } + + if page.Page == 0 { + page.Page = utils.DefaultPage + } + + if page.Limit == 0 { + page.Limit = utils.DefaultLimit + } + + // Create a custom writer to capture and process response body + buffer := new(bytes.Buffer) + writer := &bodyWriter{body: buffer, ResponseWriter: c.Writer} + c.Writer = writer + + // Set the pagination information for routes who need it + c.Set("page", page) + c.Next() + + // Get the pagination information from route after processing + metaValue, paginationExists := c.Get("paginationMeta") + + // Handle only 200 responses with paginationMeta + if paginationExists && c.Writer.Status() == 200 { + originalBody := writer.body.Bytes() + var metaObject *models.PaginationMeta + + // Try the body with different possible unmarshalling before failing + var licenseRes models.LicenseResponse + var obligationRes models.ObligationResponse + var auditRes models.AuditResponse + isLicenseRes := false + isObligationRes := false + isAuditRes := false + responseModel, _ := c.Get("responseModel") + switch responseModel.(type) { + case *models.LicenseResponse: + err = json.Unmarshal(originalBody, &licenseRes) + isLicenseRes = true + metaObject = licenseRes.Meta + case *models.ObligationResponse: + err = json.Unmarshal(originalBody, &obligationRes) + isObligationRes = true + metaObject = obligationRes.Meta + case *models.AuditResponse: + err = json.Unmarshal(originalBody, &auditRes) + isAuditRes = true + metaObject = auditRes.Meta + default: + err = fmt.Errorf("unknown response model type") + } + if err != nil { + log.Fatalf("Error marshalling new body: %s", err) + } + + paginationMeta := metaValue.(models.PaginationMeta) + + // Get the query params from the request + params := c.Request.URL.Query() + + metaObject.Page = page.Page + metaObject.Limit = page.Limit + metaObject.ResourceCount = paginationMeta.ResourceCount + metaObject.TotalPages = int(math.Ceil(float64(paginationMeta.ResourceCount) / float64(page.Limit))) + // Can go next + if metaObject.Page < metaObject.TotalPages { + params.Set("page", strconv.FormatInt(int64(metaObject.Page+1), 10)) + c.Request.URL.RawQuery = params.Encode() + + metaObject.Next = c.Request.URL.String() + } + // Can go previous + if metaObject.Page > 1 { + params.Set("page", strconv.FormatInt(int64(metaObject.Page-1), 10)) + c.Request.URL.RawQuery = params.Encode() + + metaObject.Previous = c.Request.URL.String() + } + + // Marshal the new body + var newBody []byte + var err error + if isLicenseRes { + newBody, err = json.Marshal(licenseRes) + } else if isObligationRes { + newBody, err = json.Marshal(obligationRes) + } else if isAuditRes { + newBody, err = json.Marshal(auditRes) + } + if err != nil { + log.Fatalf("Error marshalling new body: %s", err.Error()) + } + _, err = c.Writer.WriteString(string(newBody)) + if err != nil { + log.Fatalf("Error writing new body: %s", err.Error()) + } + } else { + // Write the original body for non-paginated responses + _, err := c.Writer.WriteString(writer.body.String()) + if err != nil { + log.Fatalf("Error writing new body: %s", err.Error()) + } + } + } +} + +// bodyWriter is a custom writer to capture and process response body. +type bodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +// Write is a custom write function to capture and process response body. +func (w bodyWriter) Write(b []byte) (int, error) { + return w.body.Write(b) +} diff --git a/pkg/models/types.go b/pkg/models/types.go index 6cbc3dc..5a1f9d2 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -87,9 +87,34 @@ type LicenseUpdate struct { // It contains information that provides context and supplementary details // about the retrieved license data. type PaginationMeta struct { - ResourceCount int `json:"resource_count" example:"20"` - Page int `json:"page,omitempty" example:"1"` - PerPage int `json:"per_page,omitempty" example:"10"` + ResourceCount int `json:"resource_count" example:"200"` + TotalPages int `json:"total_pages,omitempty" example:"20"` + Page int `json:"page,omitempty" example:"10"` + Limit int `json:"limit,omitempty" example:"10"` + Next string `json:"next,omitempty" example:"/api/v1/licenses?limit=10&page=11"` + Previous string `json:"previous,omitempty" example:"/api/v1/licenses?limit=10&page=9"` +} + +// The PaginationInput struct represents the input required for pagination. +type PaginationInput struct { + Page int `json:"page" example:"10"` + Limit int `json:"limit" example:"10"` +} + +// PaginationParse interface processes the pagination input. +type PaginationParse interface { + GetOffset() int + GetLimit() int +} + +// GetOffset returns the offset value for gorm. +func (p PaginationInput) GetOffset() int { + return (p.Page - 1) * p.Limit +} + +// GetLimit returns the limit value for gorm. +func (p PaginationInput) GetLimit() int { + return p.Limit } // LicenseResponse struct is representation of design API response of license. @@ -97,9 +122,9 @@ type PaginationMeta struct { // retrieving license information. // It is used to encapsulate license-related data in an organized manner. type LicenseResponse struct { - Status int `json:"status" example:"200"` - Data []LicenseDB `json:"data"` - Meta PaginationMeta `json:"paginationmeta"` + Status int `json:"status" example:"200"` + Data []LicenseDB `json:"data"` + Meta *PaginationMeta `json:"paginationmeta"` } // The LicenseError struct represents an error response related to license operations. @@ -201,9 +226,9 @@ type ChangeLogResponse struct { // AuditResponse represents the response format for audit data. type AuditResponse struct { - Status int `json:"status" example:"200"` - Data []Audit `json:"data"` - Meta PaginationMeta `json:"paginationmeta"` + Status int `json:"status" example:"200"` + Data []Audit `json:"data"` + Meta *PaginationMeta `json:"paginationmeta"` } // Obligation represents an obligation record in the database. @@ -247,9 +272,9 @@ type UpdateObligation struct { // ObligationResponse represents the response format for obligation data. type ObligationResponse struct { - Status int `json:"status" example:"200"` - Data []Obligation `json:"data"` - Meta PaginationMeta `json:"paginationmeta"` + Status int `json:"status" example:"200"` + Data []Obligation `json:"data"` + Meta *PaginationMeta `json:"paginationmeta"` } // ObligationMap represents the mapping between an obligation and a license. diff --git a/pkg/utils/util.go b/pkg/utils/util.go index de71ccf..ae2360f 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -14,6 +14,8 @@ import ( "strings" "time" + "gorm.io/gorm" + "golang.org/x/crypto/bcrypt" "github.com/gin-gonic/gin" @@ -21,6 +23,13 @@ import ( "github.com/fossology/LicenseDb/pkg/models" ) +var ( + // DefaultPage Set default page to 1 + DefaultPage = 1 + // DefaultLimit Set default max limit to 20 + DefaultLimit = 20 +) + // The Converter function takes an input of type models.LicenseJson and converts it into a // corresponding models.LicenseDB object. // It performs several field assignments and transformations to create the LicenseDB object, @@ -146,3 +155,31 @@ func HashPassword(user *models.User) error { func VerifyPassword(inputPassword, dbPassword string) error { return bcrypt.CompareHashAndPassword([]byte(dbPassword), []byte(inputPassword)) } + +// PreparePaginateResponse prepares the pagination response for the API. +// It gets the count of total rows and sets the pagination parameters, also +// updates the query limit and offset and update the "paginationMeta" and +// "responseModel" in gin.Context for middleware to process. +func PreparePaginateResponse(c *gin.Context, query *gorm.DB, + responseModel interface{}) models.PaginationInput { + var totalRows int64 + query.Count(&totalRows) + + pageVar, exists := c.Get("page") + if !exists { + pageVar = models.PaginationInput{ + Page: DefaultPage, + Limit: DefaultLimit, + } + } + pagination := pageVar.(models.PaginationInput) + + query.Offset(pagination.GetOffset()).Limit(pagination.GetLimit()) + + var paginationMeta models.PaginationMeta + paginationMeta.ResourceCount = int(totalRows) + + c.Set("paginationMeta", paginationMeta) + c.Set("responseModel", responseModel) + return pagination +}