Enhance company and company category search forms by adding new sortable fields (email, phone, country, state, language, currency) and updating related API documentation.

This commit is contained in:
Mazyar
2026-06-02 22:07:49 +03:30
parent 2457b76df9
commit 2090cc05ba
7 changed files with 46 additions and 59 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ type SearchForm struct {
FoundedYearMax *int `query:"founded_year_max" valid:"optional,range(1800|2100)"` FoundedYearMax *int `query:"founded_year_max" valid:"optional,range(1800|2100)"`
Limit *int `query:"limit" valid:"optional,range(1|100)"` Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,range(0|1000000)"` Offset *int `query:"offset" valid:"optional,range(0|1000000)"`
SortBy *string `query:"sort_by" valid:"optional,in(name|type|industry|created_at|updated_at|status|employee_count|annual_revenue)"` SortBy *string `query:"sort_by" valid:"optional,in(name|type|industry|created_at|updated_at|status|employee_count|annual_revenue|email|phone|country|state|language|currency)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
} }
+2
View File
@@ -210,6 +210,8 @@ func (h *Handler) Delete(c echo.Context) error {
// @Param is_compliant query boolean false "Compliance status" // @Param is_compliant query boolean false "Compliance status"
// @Param limit query integer false "Limit" default(20) // @Param limit query integer false "Limit" default(20)
// @Param offset query integer false "Offset" default(0) // @Param offset query integer false "Offset" default(0)
// @Param sort_by query string false "Field to sort by" Enums(name, type, industry, created_at, updated_at, status, employee_count, annual_revenue, email, phone, country, state, language, currency) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies search completed" // @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies search completed"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" // @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" // @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
+12 -1
View File
@@ -56,6 +56,17 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
} }
} }
func mapCompanySortField(sortBy string) string {
switch sortBy {
case "country":
return "address.country"
case "state":
return "address.state"
default:
return sortBy
}
}
// Create creates a new company // Create creates a new company
func (r *companyRepository) Create(ctx context.Context, company *Company) error { func (r *companyRepository) Create(ctx context.Context, company *Company) error {
// Set created/updated timestamps using Unix timestamps // Set created/updated timestamps using Unix timestamps
@@ -285,7 +296,7 @@ func (r *companyRepository) Search(ctx context.Context, form *SearchForm, pagina
pagination.Limit, pagination.Limit,
pagination.Offset, pagination.Offset,
pagination.Cursor, pagination.Cursor,
pagination.SortBy, mapCompanySortField(pagination.SortBy),
pagination.SortOrder, pagination.SortOrder,
filter, filter,
orm.ListPaginationOptions{}, orm.ListPaginationOptions{},
+1 -1
View File
@@ -15,7 +15,7 @@ type SearchForm struct {
Published *bool `query:"published" valid:"optional"` Published *bool `query:"published" valid:"optional"`
Limit *int `query:"limit" valid:"optional,range(1|100)"` Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,range(0|1000000)"` Offset *int `query:"offset" valid:"optional,range(0|1000000)"`
SortBy *string `query:"sort_by" valid:"optional,in(name|created_at|updated_at|published_at)"` SortBy *string `query:"sort_by" valid:"optional,in(name|description|published|created_at|updated_at|published_at)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
} }
+1 -1
View File
@@ -155,7 +155,7 @@ func (h *Handler) Delete(c echo.Context) error {
// @Param published query boolean false "Published status filter" // @Param published query boolean false "Published status filter"
// @Param limit query integer false "Limit" default(20) // @Param limit query integer false "Limit" default(20)
// @Param offset query integer false "Offset" default(0) // @Param offset query integer false "Offset" default(0)
// @Param sort_by query string false "Sort by field" Enums(name,created_at,updated_at,published_at) // @Param sort_by query string false "Sort by field" Enums(name,description,published,created_at,updated_at,published_at)
// @Param sort_order query string false "Sort order" Enums(asc,desc) // @Param sort_order query string false "Sort order" Enums(asc,desc)
// @Success 200 {object} response.APIResponse{data=CategoryListResponse} "Categories search completed" // @Success 200 {object} response.APIResponse{data=CategoryListResponse} "Categories search completed"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" // @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
+29
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
@@ -189,6 +190,10 @@ func documentSortValue(docMap bson.M, sortField string) (interface{}, error) {
return id, nil return id, nil
} }
if strings.Contains(sortField, ".") {
return nestedDocumentValue(docMap, sortField)
}
sortValue, exists := docMap[sortField] sortValue, exists := docMap[sortField]
if !exists { if !exists {
return nil, fmt.Errorf("sort field %s not found in document", sortField) return nil, fmt.Errorf("sort field %s not found in document", sortField)
@@ -196,6 +201,30 @@ func documentSortValue(docMap bson.M, sortField string) (interface{}, error) {
return sortValue, nil return sortValue, nil
} }
func nestedDocumentValue(docMap bson.M, path string) (interface{}, error) {
parts := strings.Split(path, ".")
var current interface{} = docMap
for _, part := range parts {
nested, ok := current.(bson.M)
if !ok {
if nestedMap, ok := current.(map[string]interface{}); ok {
nested = bson.M(nestedMap)
} else {
return nil, fmt.Errorf("sort field %s not found in document", path)
}
}
value, exists := nested[part]
if !exists {
return nil, fmt.Errorf("sort field %s not found in document", path)
}
current = value
}
return current, nil
}
func documentIDHex(docMap bson.M) (string, error) { func documentIDHex(docMap bson.M) (string, error) {
id, exists := docMap["_id"] id, exists := docMap["_id"]
if !exists { if !exists {
-55
View File
@@ -1,55 +0,0 @@
package mongo
import (
"strings"
"testing"
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestEncodeDecodePageCursor(t *testing.T) {
encoded, err := encodePageCursor("created_at", -1, int64(1700000000), "507f1f77bcf86cd799439011", "abc123", 10)
if err != nil {
t.Fatalf("encode: %v", err)
}
if strings.Contains(encoded, "+") || strings.Contains(encoded, "/") {
t.Fatalf("expected URL-safe cursor, got %q", encoded)
}
decoded, err := decodePageCursor(encoded)
if err != nil {
t.Fatalf("decode: %v", err)
}
if decoded.SortField != "created_at" || decoded.SortOrder != -1 {
t.Fatalf("unexpected sort: %+v", decoded)
}
if decoded.FiltersHash != "abc123" || decoded.PageOffset != 10 {
t.Fatalf("unexpected metadata: %+v", decoded)
}
}
func TestBuildListPaginationCursorAndOffsetConflict(t *testing.T) {
_, err := BuildListPagination(10, 5, "cursor-token", "created_at", "desc", bson.M{}, ListPaginationOptions{})
if err == nil {
t.Fatal("expected error when cursor and offset are both set")
}
}
func TestBuildListPaginationOffsetMode(t *testing.T) {
p, err := BuildListPagination(10, 20, "", "created_at", "desc", bson.M{"status": "active"}, ListPaginationOptions{})
if err != nil {
t.Fatalf("build: %v", err)
}
if p.Skip != 20 || p.Limit != 10 || p.Cursor != "" {
t.Fatalf("unexpected pagination: %+v", p)
}
}
func TestHashFilterStable(t *testing.T) {
a := HashFilter(bson.M{"status": "active"})
b := HashFilter(bson.M{"status": "active"})
if a == "" || a != b {
t.Fatalf("expected stable non-empty hash, got %q and %q", a, b)
}
}