hybrid pagination

This commit is contained in:
Mazyar
2026-05-17 17:21:36 +03:30
parent 42d0a47226
commit 6dac5b482a
39 changed files with 696 additions and 386 deletions
+7 -1
View File
@@ -179,10 +179,16 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to search CMS entries")
}
+17 -4
View File
@@ -6,6 +6,7 @@ import (
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -17,7 +18,7 @@ type Repository interface {
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*CMS, error)
GetByKey(ctx context.Context, key string) (*CMS, error)
Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error)
Search(ctx context.Context, search *SearchCMSForm, pagination *response.Pagination) (*orm.PaginatedResult[CMS], error)
}
// cmsRepository implements the Repository interface using the MongoDB ORM
@@ -169,7 +170,7 @@ func (r *cmsRepository) Delete(ctx context.Context, id string) error {
}
// Search searches CMS entries with filters and pagination
func (r *cmsRepository) Search(ctx context.Context, search *SearchCMSForm, pagination orm.Pagination) (*orm.PaginatedResult[CMS], error) {
func (r *cmsRepository) Search(ctx context.Context, search *SearchCMSForm, pagination *response.Pagination) (*orm.PaginatedResult[CMS], error) {
filter := bson.M{}
if search.Search != nil && *search.Search != "" {
@@ -182,8 +183,20 @@ func (r *cmsRepository) Search(ctx context.Context, search *SearchCMSForm, pagin
filter["key"] = bson.M{"$regex": search.Key, "$options": "i"}
}
// Use ORM to find CMS entries
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search CMS entries", map[string]interface{}{
"error": err.Error(),
+2 -14
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
)
@@ -197,18 +196,7 @@ func (s *cmsService) Search(ctx context.Context, form *SearchCMSForm, pagination
"pagination": pagination,
})
// Convert response.Pagination to orm.Pagination
ormPagination := orm.Pagination{
Limit: pagination.Limit,
Skip: pagination.Offset,
SortField: pagination.SortBy,
SortOrder: 1, // Default ascending
}
if pagination.SortOrder == "desc" {
ormPagination.SortOrder = -1
}
result, err := s.repo.Search(ctx, form, ormPagination)
result, err := s.repo.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search CMS entries", map[string]interface{}{
"filters": form,
@@ -230,7 +218,7 @@ func (s *cmsService) Search(ctx context.Context, form *SearchCMSForm, pagination
return &CMSListResponse{
CMS: cmsResponses,
Meta: pagination.Response(result.TotalCount),
Meta: pagination.ListMeta(result.TotalCount, result.NextCursor, result.HasMore, result.PageOffset),
}, nil
}
+9 -1
View File
@@ -222,8 +222,16 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.SearchCompanies(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.SearchCompanies(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list companies")
}
+19 -25
View File
@@ -20,7 +20,7 @@ type Repository interface {
GetByName(ctx context.Context, name string) (*Company, error)
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error)
GetByTaxID(ctx context.Context, taxID string) (*Company, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Company, int64, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Company], error)
Delete(ctx context.Context, id string) error
UpdateStatus(ctx context.Context, id string, status CompanyStatus) error
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
@@ -194,7 +194,7 @@ func (r *companyRepository) Delete(ctx context.Context, id string) error {
}
// Search retrieves companies with search and filters
func (r *companyRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Company, int64, error) {
func (r *companyRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Company], error) {
// Build filter
filter := bson.M{}
@@ -281,34 +281,28 @@ func (r *companyRepository) Search(ctx context.Context, form *SearchForm, pagina
filter["founded_year"] = rangeFilter
}
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find users
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
r.logger.Error("Failed to list users", map[string]interface{}{
return nil, err
}
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search companies", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
// UpdateStatus updates company status
+4 -5
View File
@@ -321,17 +321,16 @@ func (s *companyService) Delete(ctx context.Context, id string) error {
func (s *companyService) SearchCompanies(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CompanyListResponse, error) {
// Get companies using search
companies, total, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search companies", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search companies")
return nil, err
}
// Convert to responses
var companyResponses []*CompanyResponse
for _, company := range companies {
for _, company := range page.Items {
// Get category details for response
categoryDetails, err := s.categoryService.GetByIDs(ctx, company.Tags.Categories)
if err != nil {
@@ -354,7 +353,7 @@ func (s *companyService) SearchCompanies(ctx context.Context, form *SearchForm,
return &CompanyListResponse{
Companies: companyResponses,
Meta: pagination.Response(total),
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
+9 -1
View File
@@ -169,8 +169,16 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.SearchCategories(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.SearchCategories(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to search categories")
}
+16 -22
View File
@@ -18,7 +18,7 @@ type Repository interface {
GetByID(ctx context.Context, id string) (*Category, error)
GetByIDs(ctx context.Context, ids []string) ([]Category, error)
GetByName(ctx context.Context, name string) (*Category, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Category], error)
Delete(ctx context.Context, id string) error
TogglePublish(ctx context.Context, id string) (bool, error)
}
@@ -178,7 +178,7 @@ func (r *categoryRepository) Delete(ctx context.Context, id string) error {
}
// Search retrieves categories with search and filters
func (r *categoryRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error) {
func (r *categoryRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Category], error) {
// Build filter
filter := bson.M{}
@@ -198,34 +198,28 @@ func (r *categoryRepository) Search(ctx context.Context, form *SearchForm, pagin
filter["published"] = *form.Published
}
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find categories
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search categories", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
// TogglePublish toggles category publish status
+4 -5
View File
@@ -177,23 +177,22 @@ func (s *categoryService) Delete(ctx context.Context, id string) error {
// SearchCategories searches categories with filters
func (s *categoryService) SearchCategories(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CategoryListResponse, error) {
// Get categories using search
categories, total, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search categories", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search categories")
return nil, err
}
// Convert to responses
var categoryResponses []*CategoryResponse
for _, category := range categories {
for _, category := range page.Items {
categoryResponses = append(categoryResponses, category.ToResponse())
}
return &CategoryListResponse{
Categories: categoryResponses,
Meta: pagination.Response(total),
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
+7 -1
View File
@@ -113,10 +113,16 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to search contacts")
}
+17 -6
View File
@@ -6,6 +6,7 @@ import (
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
@@ -20,7 +21,7 @@ type ContactRepository interface {
GetByID(ctx context.Context, id string) (*Contact, error)
// Search searches contacts with filters and pagination
Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error)
Search(ctx context.Context, filters SearchContactsForm, pagination *response.Pagination) (*orm.PaginatedResult[Contact], error)
// UpdateStatus updates the status of a contact
UpdateStatus(ctx context.Context, id string, status ContactStatus, adminNotes *string, resolvedBy *string) error
@@ -87,20 +88,17 @@ func (r *contactRepository) GetByID(ctx context.Context, id string) (*Contact, e
}
// Search searches contacts with filters and pagination
func (r *contactRepository) Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error) {
func (r *contactRepository) Search(ctx context.Context, filters SearchContactsForm, pagination *response.Pagination) (*orm.PaginatedResult[Contact], error) {
filter := bson.M{}
// Apply text search if provided
if filters.Search != nil && *filters.Search != "" {
filter["$text"] = bson.M{"$search": *filters.Search}
}
// Apply status filter
if filters.Status != nil && *filters.Status != "" {
filter["status"] = *filters.Status
}
// Apply date range filters
if filters.DateFrom != nil || filters.DateTo != nil {
dateFilter := bson.M{}
if filters.DateFrom != nil {
@@ -112,7 +110,20 @@ func (r *contactRepository) Search(ctx context.Context, filters SearchContactsFo
filter["created_at"] = dateFilter
}
return r.repo.FindAll(ctx, filter, pagination)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
return r.repo.FindAll(ctx, filter, mongoPagination)
}
// UpdateStatus updates the status of a contact
+2 -14
View File
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/notification"
"tm/pkg/response"
)
@@ -155,18 +154,7 @@ func (s *contactService) Search(ctx context.Context, form *SearchContactsForm, p
return nil, fmt.Errorf("invalid date range: date_from must be before date_to")
}
// Convert response.Pagination to orm.Pagination
ormPagination := orm.Pagination{
Limit: pagination.Limit,
Skip: pagination.Offset,
SortField: pagination.SortBy,
SortOrder: 1, // Default ascending
}
if pagination.SortOrder == "desc" {
ormPagination.SortOrder = -1
}
result, err := s.repo.Search(ctx, *form, ormPagination)
result, err := s.repo.Search(ctx, *form, pagination)
if err != nil {
s.logger.Error("Failed to search contacts", map[string]interface{}{
"filters": form,
@@ -188,7 +176,7 @@ func (s *contactService) Search(ctx context.Context, form *SearchContactsForm, p
return &ContactListResponse{
Contacts: contactResponses,
Meta: pagination.Response(result.TotalCount),
Meta: pagination.ListMeta(result.TotalCount, result.NextCursor, result.HasMore, result.PageOffset),
}, nil
}
+7 -1
View File
@@ -191,10 +191,16 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list customers")
}
+17 -12
View File
@@ -24,7 +24,7 @@ type Repository interface {
GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error)
GetByCompanies(ctx context.Context, companies []string) ([]Customer, error)
GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error)
Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, error)
Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*orm.PaginatedResult[Customer], error)
Delete(ctx context.Context, id string) error
}
@@ -329,8 +329,7 @@ func (r *customerRepository) Delete(ctx context.Context, id string) error {
}
// Search retrieves customers with search and filters
func (r *customerRepository) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, error) {
// Build filter
func (r *customerRepository) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*orm.PaginatedResult[Customer], error) {
filter := bson.M{}
if form.Search != nil {
@@ -349,23 +348,29 @@ func (r *customerRepository) Search(ctx context.Context, form *SearchCustomersFo
filter["company_id"] = *form.CompanyID
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(pagination.SortBy, pagination.SortOrder)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": form.Search,
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
// UpdateStatus updates customer status
+6 -6
View File
@@ -218,16 +218,16 @@ func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResp
// Search searches for customers
func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) {
customers, total, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search customers")
return nil, err
}
var customerResponses []*CustomerResponse
for _, customer := range customers {
for _, customer := range page.Items {
companies, err := s.loadCompaniesForCustomer(ctx, &customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
@@ -241,12 +241,12 @@ func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm,
return &CustomerListResponse{
Customers: customerResponses,
Meta: pagination.Response(total),
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
func (s *customerService) SearchNotification(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]*CustomerNotificationResponse, error) {
customers, _, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
@@ -255,7 +255,7 @@ func (s *customerService) SearchNotification(ctx context.Context, form *SearchCu
}
var customerResponses []*CustomerNotificationResponse
for _, customer := range customers {
for _, customer := range page.Items {
customerResponses = append(customerResponses, customer.ToNotificationResponse())
}
+1 -1
View File
@@ -62,7 +62,7 @@ type (
type FeedbackListResponse struct {
Feedback []*FeedbackResponse `json:"feedback"`
Meta *response.Meta `json:"meta"`
Meta *response.Meta `json:"-"`
}
func (f *Feedback) ToResponse(tender *tenderResponse, company *companyResponse) *FeedbackResponse {
+20 -5
View File
@@ -60,12 +60,20 @@ func (h *Handler) Search(c echo.Context) error {
return response.ValidationError(c, "Invalid request data", err.Error())
}
feedbackList, err := h.service.Search(c.Request().Context(), *form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
feedbackList, err := h.service.Search(c.Request().Context(), *form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to retrieve feedback list")
}
return response.Success(c, feedbackList.Feedback, "feedback list retrieved successfully")
return response.SuccessWithMeta(c, feedbackList, feedbackList.Meta, "feedback list retrieved successfully")
}
// GetFeedbackByID retrieves feedback by ID with loaded related data
@@ -217,13 +225,20 @@ func (h *Handler) PublicList(c echo.Context) error {
}
form.Company = &companyID
// Call service
result, err := h.service.Search(c.Request().Context(), *form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), *form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list feedback")
}
return response.Success(c, result, "Feedback list retrieved successfully")
return response.SuccessWithMeta(c, result, result.Meta, "Feedback list retrieved successfully")
}
// PublicGetFeedback retrieves feedback by ID
+19 -25
View File
@@ -16,7 +16,7 @@ type Repository interface {
// Basic CRUD operations
Create(ctx context.Context, feedback *Feedback) error
Update(ctx context.Context, feedback *Feedback) error
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) ([]Feedback, int64, error)
Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Feedback], error)
GetByID(ctx context.Context, id string) (*Feedback, error)
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
GetByTenderID(ctx context.Context, tenderID, companyID string) (*Feedback, error)
@@ -172,37 +172,31 @@ func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
}
// Search searches feedback based on criteria
func (r *feedbackRepository) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) ([]Feedback, int64, error) {
func (r *feedbackRepository) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*orm.PaginatedResult[Feedback], error) {
filter := r.buildSearchFilter(criteria)
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find users
result, err := r.repo.FindAll(ctx, filter, paginationBuilder)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
r.logger.Error("Failed to list users", map[string]interface{}{
return nil, err
}
result, err := r.repo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search feedback", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
// Stats calculates feedback statistics for a company
+4 -10
View File
@@ -152,7 +152,7 @@ func (s *feedbackService) Get(ctx context.Context, id string) (*FeedbackResponse
}
func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagination *response.Pagination) (*FeedbackListResponse, error) {
feedbacks, total, err := s.feedbackRepo.Search(ctx, criteria, pagination)
page, err := s.feedbackRepo.Search(ctx, criteria, pagination)
if err != nil {
s.logger.Error("Failed to list feedback", map[string]interface{}{
"criteria": criteria,
@@ -163,16 +163,10 @@ func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagin
return nil, err
}
meta := &response.Meta{
Total: int(total),
Limit: pagination.Limit,
Offset: pagination.Offset,
Page: (pagination.Offset / pagination.Limit) + 1,
Pages: int((total + int64(pagination.Limit) - 1) / int64(pagination.Limit)),
}
meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset)
result := make([]*FeedbackResponse, len(feedbacks))
for i, feedback := range feedbacks {
result := make([]*FeedbackResponse, len(page.Items))
for i, feedback := range page.Items {
tender, company, _ := s.getDetails(ctx, feedback.TenderID, *feedback.CompanyID)
result[i] = feedback.ToResponse(tender, company)
}
+9 -1
View File
@@ -185,8 +185,16 @@ func (h *Handler) SearchInquiries(c echo.Context) error {
}
// Call service
inquiries, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
inquiries, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to search inquiries")
}
+16 -22
View File
@@ -16,7 +16,7 @@ type Repository interface {
Create(ctx context.Context, inquiry *Inquiry) error
GetByID(ctx context.Context, id string) (*Inquiry, error)
UpdateStatus(ctx context.Context, id string, status, reason, changedBy, description string) error
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) ([]Inquiry, int64, error)
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*orm.PaginatedResult[Inquiry], error)
Delete(ctx context.Context, id string) error
CheckPendingInquiry(ctx context.Context, email string) (*Inquiry, error)
CheckPendingInquiryByPhone(ctx context.Context, phone string) (*Inquiry, error)
@@ -206,7 +206,7 @@ func (r *inquiryRepository) CheckPendingInquiryByPhone(ctx context.Context, phon
}
// Search retrieves inquiries with pagination and filters
func (r *inquiryRepository) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) ([]Inquiry, int64, error) {
func (r *inquiryRepository) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*orm.PaginatedResult[Inquiry], error) {
filter := bson.M{}
// Add search filter
@@ -223,34 +223,28 @@ func (r *inquiryRepository) Search(ctx context.Context, form *SearchInquiriesFor
filter["status"] = *form.Status
}
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find inquiries
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search inquiries", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
// Delete deletes an inquiry
+5 -7
View File
@@ -273,29 +273,27 @@ func (s *inquiryService) Search(ctx context.Context, form *SearchInquiriesForm,
"offset": pagination.Offset,
})
// Get inquiries
inquiries, total, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search inquiries", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search inquiries")
return nil, err
}
// Convert to responses
var inquiryResponses []*InquiryResponse
for _, inquiry := range inquiries {
for _, inquiry := range page.Items {
inquiryResponses = append(inquiryResponses, inquiry.ToResponse())
}
s.logger.Info("Inquiries search completed successfully", map[string]interface{}{
"total_found": total,
"total_found": page.TotalCount,
"returned": len(inquiryResponses),
})
return &InquiryListResponse{
Inquiries: inquiryResponses,
Meta: pagination.Response(total),
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
+21 -7
View File
@@ -87,11 +87,16 @@ func (h *NotificationHandler) GetNotifications(c echo.Context) error {
return response.ValidationError(c, err.Error(), "")
}
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
// Call service
notifications, err := h.service.GetNotifications(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to get notifications")
}
@@ -141,11 +146,16 @@ func (h *NotificationHandler) AdminNotifications(c echo.Context) error {
form.Recipient = userID
form.Status = "sent"
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
// Call service
notifications, err := h.service.GetNotifications(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to get notifications")
}
@@ -254,12 +264,16 @@ func (h *NotificationHandler) CustomerNotifications(c echo.Context) error {
form.Recipient = userID
form.Status = "sent"
pagination := response.NewPagination(c)
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
// Call service
notifications, err := h.service.GetNotifications(c.Request().Context(), form, pagination)
if err != nil {
// Log the actual error for debugging
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
c.Logger().Error("GetNotifications failed", map[string]interface{}{
"error": err.Error(),
"status": form.Status,
+15 -35
View File
@@ -11,7 +11,6 @@ import (
"go.mongodb.org/mongo-driver/v2/bson"
mongopkg "go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
var (
@@ -45,7 +44,7 @@ type Notification struct {
type Repository interface {
Create(ctx context.Context, notification *Notification) error
GetByID(ctx context.Context, id string) (*Notification, error)
GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) ([]Notification, int64, error)
GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error)
Update(ctx context.Context, notification *Notification) error
MarkAsSeen(ctx context.Context, notificationID, userID string) error
MarkAllAsSeen(ctx context.Context, userID string) error
@@ -108,7 +107,7 @@ func (r *notificationRepository) GetByID(ctx context.Context, id string) (*Notif
}
// GetByUserID retrieves notifications for a specific user with filters
func (r *notificationRepository) GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) ([]Notification, int64, error) {
func (r *notificationRepository) GetByUserID(ctx context.Context, userID string, status string, seen *bool, priority, eventType, notificationType string, pagination *response.Pagination) (*orm.PaginatedResult[Notification], error) {
filter := bson.M{"user_id": userID}
if status != "" {
@@ -131,48 +130,29 @@ func (r *notificationRepository) GetByUserID(ctx context.Context, userID string,
filter["type"] = notificationType
}
// Get total count
total, err := r.collection.CountDocuments(ctx, filter)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
r.logger.Error("Failed to count notifications", map[string]interface{}{
"error": err.Error(),
"user_id": userID,
})
return nil, 0, err
return nil, err
}
// Build find options
findOptions := options.Find()
if pagination.Limit > 0 {
findOptions.SetLimit(int64(pagination.Limit))
}
if pagination.Offset > 0 {
findOptions.SetSkip(int64(pagination.Offset))
}
// Sort by created_at descending
findOptions.SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, filter, findOptions)
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to find notifications", map[string]interface{}{
"error": err.Error(),
"user_id": userID,
})
return nil, 0, err
}
defer cursor.Close(ctx)
var notifications []Notification
if err := cursor.All(ctx, &notifications); err != nil {
r.logger.Error("Failed to decode notifications", map[string]interface{}{
"error": err.Error(),
"user_id": userID,
})
return nil, 0, err
return nil, err
}
return notifications, total, nil
return result, nil
}
// Update updates a notification
+3 -15
View File
@@ -194,7 +194,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
"offset": pagination.Offset,
})
notifications, total, err := s.repository.GetByUserID(ctx, req.Recipient, req.Status, req.Seen, req.Priority, req.EventType, req.Type, pagination)
page, err := s.repository.GetByUserID(ctx, req.Recipient, req.Status, req.Seen, req.Priority, req.EventType, req.Type, pagination)
if err != nil {
s.logger.Error("Failed to get notifications", map[string]interface{}{
"error": err.Error(),
@@ -203,7 +203,7 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
}
notificationsResponse := make([]*NotificationResponse, 0)
for _, notification := range notifications {
for _, notification := range page.Items {
usr, _ := s.userService.GetUserByID(ctx, notification.UserID)
cust, _ := s.customerService.GetByID(ctx, notification.UserID)
@@ -250,21 +250,9 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF
})
}
// Calculate total pages
totalPages := int(total) / pagination.Limit
if int(total)%pagination.Limit > 0 {
totalPages++
}
return &NotificationListResponse{
Notifications: notificationsResponse,
Meta: &response.Meta{
Total: int(total),
Limit: pagination.Limit,
Offset: pagination.Offset,
Page: (pagination.Offset / pagination.Limit) + 1,
Pages: totalPages,
},
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
+24 -13
View File
@@ -177,11 +177,15 @@ func (h *TenderHandler) Search(c echo.Context) error {
return response.ValidationError(c, "Validation failed", err.Error())
}
// Call service
tenders, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
if isInvalidPaginationCursor(err) {
return response.ValidationError(c, "Invalid pagination cursor", err.Error())
return response.PaginationBadRequest(c, err)
}
tenders, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list tenders")
}
@@ -259,10 +263,15 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
}
form.CustomerID = customerID
result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
if isInvalidPaginationCursor(err) {
return response.ValidationError(c, "Invalid pagination cursor", err.Error())
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to retrieve tenders")
}
@@ -335,10 +344,15 @@ func (h *TenderHandler) RecommendTenders(c echo.Context) error {
}
form.CustomerID = customerID
result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
if isInvalidPaginationCursor(err) {
return response.ValidationError(c, "Invalid pagination cursor", err.Error())
return response.PaginationBadRequest(c, err)
}
result, err := h.service.Recommend(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to retrieve tenders")
}
@@ -664,6 +678,3 @@ func (h *TenderHandler) GetAllAISummaries(c echo.Context) error {
return response.Success(c, summaries, "AI summaries retrieved successfully")
}
func isInvalidPaginationCursor(err error) bool {
return errors.Is(err, orm.ErrInvalidCursor)
}
+14 -15
View File
@@ -40,8 +40,6 @@ func collectionName() string {
return "tenders"
}
// deepOffsetSkipCountThreshold skips CountDocuments when offset pagination goes this deep.
const deepOffsetSkipCountThreshold = 1000
// SearchPageResult is the paginated outcome of a tender search/list query.
type SearchPageResult struct {
@@ -49,6 +47,7 @@ type SearchPageResult struct {
TotalCount int64 // -1 when count was skipped
NextCursor string
HasMore bool
PageOffset int
}
// tenderSearchListProjection excludes large fields not needed for list/search API payloads.
@@ -370,21 +369,20 @@ func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, paginat
sortOrder = pagination.SortOrder
}
skipCount := pagination.Cursor != "" || pagination.Offset >= deepOffsetSkipCountThreshold
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
SortBy(sort, sortOrder).
Projection(tenderSearchListProjection()).
SkipCount(skipCount)
if pagination.Cursor != "" {
paginationBuilder.Cursor(pagination.Cursor)
} else {
paginationBuilder.Skip(pagination.Offset)
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
sort,
sortOrder,
filter,
orm.ListPaginationOptions{Projection: tenderSearchListProjection()},
)
if err != nil {
return nil, err
}
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to search tenders", map[string]interface{}{
"error": err.Error(),
@@ -397,6 +395,7 @@ func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, paginat
TotalCount: result.TotalCount,
NextCursor: result.NextCursor,
HasMore: result.HasMore,
PageOffset: result.PageOffset,
}, nil
}
+4 -4
View File
@@ -648,7 +648,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore),
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
@@ -667,7 +667,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore),
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
},
nil
}
@@ -690,7 +690,7 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore),
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
},
nil
}
@@ -757,7 +757,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
return &SearchResponse{
Tenders: tenderResponses,
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore),
Metadata: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
+9 -1
View File
@@ -311,8 +311,16 @@ func (h *Handler) ListUsers(c echo.Context) error {
}
// Call service
users, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
pagination, err := response.NewPagination(c)
if err != nil {
return response.PaginationBadRequest(c, err)
}
users, err := h.service.Search(c.Request().Context(), form, pagination)
if err != nil {
if response.IsListPaginationError(err) {
return response.PaginationBadRequest(c, err)
}
return response.InternalServerError(c, "Failed to list users")
}
+17 -23
View File
@@ -22,7 +22,7 @@ type Repository interface {
GetByEmail(ctx context.Context, email string) (*User, error)
GetByUsername(ctx context.Context, username string) (*User, error)
GetByIDs(ctx context.Context, userIDs []string) ([]User, error)
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error)
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*orm.PaginatedResult[User], error)
}
// userRepository implements the Repository interface using the MongoDB ORM
@@ -280,8 +280,8 @@ func (r *userRepository) GetByIDs(ctx context.Context, userIDs []string) ([]User
return users.Items, nil
}
// List retrieves users with pagination
func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error) {
// Search retrieves users with pagination
func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*orm.PaginatedResult[User], error) {
filter := bson.M{}
if form.Search != nil {
@@ -300,32 +300,26 @@ func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagi
filter["role"] = form.Role
}
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
mongoPagination, err := orm.BuildListPagination(
pagination.Limit,
pagination.Offset,
pagination.Cursor,
pagination.SortBy,
pagination.SortOrder,
filter,
orm.ListPaginationOptions{},
)
if err != nil {
return nil, err
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find users
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
if err != nil {
r.logger.Error("Failed to list users", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return result, nil
}
+4 -5
View File
@@ -278,23 +278,22 @@ func (s *userService) GetUsersByIDs(ctx context.Context, userIDs []string) (*Use
// Search searches users with search and filters
func (s *userService) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) {
// Get users
users, total, err := s.repository.Search(ctx, form, pagination)
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to list users", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to list users")
return nil, err
}
// Convert to responses
var userResponses []*UserResponse
for _, user := range users {
for _, user := range page.Items {
userResponses = append(userResponses, user.ToResponse())
}
return &UserListResponse{
Users: userResponses,
Meta: pagination.Response(total),
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}