843e1508df
continuous-integration/drone/push Build is passing
- Updated the tender search handler to include a new `include_total` query parameter, allowing clients to request total count and pagination metadata for search results. - Refactored the `tenderSearchListProjection` to exclude heavy fields from the list response, optimizing data retrieval. - Modified the `buildSearchFilter` method to utilize the `processing_metadata.documents_scraped` flag for filtering, improving search accuracy. - Added unit tests for the new pagination features and search filter logic, ensuring robust validation of the search functionality. This update significantly enhances the tender search experience by providing more flexible pagination options and improving the efficiency of data handling.
223 lines
5.8 KiB
Go
223 lines
5.8 KiB
Go
package response
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/labstack/echo/v4"
|
|
|
|
orm "tm/pkg/mongo"
|
|
)
|
|
|
|
// APIResponse represents a standard API response structure
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error *APIError `json:"error,omitempty"`
|
|
Meta *Meta `json:"meta,omitempty"`
|
|
}
|
|
|
|
// APIError represents error details in API response
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|
|
|
|
// Meta represents metadata for paginated responses
|
|
type Meta struct {
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
Page int `json:"page"`
|
|
Pages int `json:"pages"`
|
|
HasMore bool `json:"has_more"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
}
|
|
|
|
type Pagination struct {
|
|
Limit int `query:"limit" valid:"optional,range(1|100)" default:"10"`
|
|
Offset int `query:"offset" valid:"optional,range(0|1000000)" default:"0"`
|
|
Cursor string `query:"cursor" valid:"optional"`
|
|
SortBy string `query:"sort_by" valid:"optional"`
|
|
SortOrder string `query:"sort_order" valid:"optional,in(asc|desc)"`
|
|
IncludeTotal bool `query:"include_total" valid:"optional"`
|
|
}
|
|
|
|
// Success returns a successful response
|
|
func Success(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// SuccessWithMeta returns a successful response with metadata
|
|
func SuccessWithMeta(c echo.Context, data interface{}, meta *Meta, message string) error {
|
|
return c.JSON(http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
Meta: meta,
|
|
})
|
|
}
|
|
|
|
// Created returns a 201 Created response
|
|
func Created(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusCreated, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// Accepted returns a 202 Accepted response (for async operations)
|
|
func Accepted(c echo.Context, data interface{}, message string) error {
|
|
return c.JSON(http.StatusAccepted, APIResponse{
|
|
Success: true,
|
|
Message: message,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// BadRequest returns a 400 Bad Request response
|
|
func BadRequest(c echo.Context, message, details string) error {
|
|
return c.JSON(http.StatusBadRequest, APIResponse{
|
|
Success: false,
|
|
Message: "Bad Request",
|
|
Error: &APIError{
|
|
Code: "BAD_REQUEST",
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Unauthorized returns a 401 Unauthorized response
|
|
func Unauthorized(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusUnauthorized, APIResponse{
|
|
Success: false,
|
|
Message: "Unauthorized",
|
|
Error: &APIError{
|
|
Code: "UNAUTHORIZED",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Forbidden returns a 403 Forbidden response
|
|
func Forbidden(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusForbidden, APIResponse{
|
|
Success: false,
|
|
Message: "Forbidden",
|
|
Error: &APIError{
|
|
Code: "FORBIDDEN",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// NotFound returns a 404 Not Found response
|
|
func NotFound(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusNotFound, APIResponse{
|
|
Success: false,
|
|
Message: "Not Found",
|
|
Error: &APIError{
|
|
Code: "NOT_FOUND",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Conflict returns a 409 Conflict response
|
|
func Conflict(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusConflict, APIResponse{
|
|
Success: false,
|
|
Message: "Conflict",
|
|
Error: &APIError{
|
|
Code: "CONFLICT",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// TooManyRequests returns a 429 Too Many Requests response
|
|
func TooManyRequests(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusTooManyRequests, APIResponse{
|
|
Success: false,
|
|
Message: "Too Many Requests",
|
|
Error: &APIError{
|
|
Code: "TOO_MANY_REQUESTS",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ValidationError returns a 422 Unprocessable Entity response
|
|
func ValidationError(c echo.Context, message, details string) error {
|
|
return c.JSON(http.StatusUnprocessableEntity, APIResponse{
|
|
Success: false,
|
|
Message: "Validation Error",
|
|
Error: &APIError{
|
|
Code: "VALIDATION_ERROR",
|
|
Message: message,
|
|
Details: details,
|
|
},
|
|
})
|
|
}
|
|
|
|
// InternalServerError returns a 500 Internal Server Error response
|
|
func InternalServerError(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusInternalServerError, APIResponse{
|
|
Success: false,
|
|
Message: "Internal Server Error",
|
|
Error: &APIError{
|
|
Code: "INTERNAL_SERVER_ERROR",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ServiceUnavailable returns a 503 Service Unavailable response
|
|
func ServiceUnavailable(c echo.Context, message string) error {
|
|
return c.JSON(http.StatusServiceUnavailable, APIResponse{
|
|
Success: false,
|
|
Message: "Service Unavailable",
|
|
Error: &APIError{
|
|
Code: "SERVICE_UNAVAILABLE",
|
|
Message: message,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Parse parses the form and validates it
|
|
func Parse[T any](c echo.Context) (*T, error) {
|
|
var form T
|
|
if err := c.Bind(&form); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := govalidator.ValidateStruct(form); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &form, nil
|
|
}
|
|
|
|
// PaginationBadRequest writes a 400 response for invalid list pagination parameters.
|
|
func PaginationBadRequest(c echo.Context, err error) error {
|
|
msg := "Invalid pagination parameters"
|
|
if errors.Is(err, orm.ErrInvalidCursor) {
|
|
msg = "Invalid pagination cursor"
|
|
}
|
|
return BadRequest(c, msg, err.Error())
|
|
}
|
|
|
|
// IsListPaginationError reports client-facing pagination errors from list queries.
|
|
func IsListPaginationError(err error) bool {
|
|
return IsPaginationError(err) || errors.Is(err, orm.ErrInvalidCursor) || errors.Is(err, orm.ErrInvalidPagination)
|
|
}
|