From what I have been able to find out, Huma unfortunately doesn't support array query filters like this: filters[]=filter1&filters[]=filter2
(neither leaving the brackets out, e.g. filter=filter1&filter=filter2
). I came across this Github issue that gives an example of separating the filters by comma https://github.com/danielgtaylor/huma/issues/325, so that's what we ended up doing: filters=postcode:eq:RM7%28EX,created:gt:2024-01-01
I found a way to use repeated query parameters, so that I don't need to separate them by comma after all. It can be done using the explode
option in the struct tag. So below you will find the updated implementation (I deleted the old one).
The query string will look like this: filters=postcode:eq:RM7%28EX&filters=created:gt:2024-01-01
Documenting filters
Unlike the body parameters, which one can simply specify as structs and then they get both validated and generated in the documentation, the documentation and validation for filters has to be done separately.
The documentation can simply be added under the description attribute of the Huma.Param object (under Operation):
Parameters: []*huma.Param{{
Name: "filters",
In: "query",
Description: "Filter properties by various fields.\n\n" +
"Format: field:operator:value\n\n" +
"Supported fields:\n" +
"- postcode (operator: eq)\n" +
"- created (operators: gt, lt, gte, lte)\n",
Schema: &huma.Schema{
Type: "string",
Items: &huma.Schema{
Type: "string",
Pattern: "^[a-zA-Z_]+:(eq|neq|gt|lt|gte|lte):[a-zA-Z0-9-:.]+$",
},
Examples: []any{
"postcode:eq:RM7 8EX",
"created:gt:2024-01-01",
},
},
Required: false,
}},
We can now define our PropertyFilterParams
struct for validation:
type FilterParam struct {
Field string
Operator string
Value interface{}
}
type PortfolioFilterParams struct {
Items []FilterParam
}
func (input *PortfolioQueryParams) Resolve(ctx huma.Context) []error {
equalityFields := []string{"property_id", "external_id", "address", "action_text"}
greaterSmallerFields := []string{"households", "occupants"}
dateFields := []string{}
var errs []error
for idx, f := range input.Filters {
_, err := parseAndValidateFilterItem(f, equalityFields, greaterSmallerFields, dateFields)
if err != nil {
errs = append(errs, &huma.ErrorDetail{
Location: fmt.Sprintf("query.filters[%d]", idx),
Message: err.Error(),
Value: f,
})
}
}
return errs
}
func (s *PortfolioFilterParams) Schema(registry huma.Registry) *huma.Schema {
return &huma.Schema{
Type: huma.TypeString,
}
}
func parseAndValidateFilterItem(item string, equalityFields []string, greaterSmallerFields []string, dateFields []string) (FilterParam, error) {
parts := strings.SplitN(item, ":", 3)
field := parts[0]
operator := parts[1]
value := parts[2]
if contains(equalityFields, field) {
if operator != "eq" && operator != "neq" {
return FilterParam{}, fmt.Errorf("Unsupported operator %s for field %s. Only 'eq' and 'neq' are supported.", operator, field)
}
} else if contains(greaterSmallerFields, field) {
if !validation.IsValidCompareGreaterSmallerOperator(operator) {
return FilterParam{}, fmt.Errorf("Unsupported operator %s for field %s. Supported operators: eq, neq, gt, lt, gte, lte.", operator, field)
}
} else if contains(dateFields, field) {
if !validation.IsValidCompareGreaterSmallerOperator(operator) {
return FilterParam{}, fmt.Errorf("Unsupported operator %s for field %s. Supported operators: eq, neq, gt, lt, gte, lte.", operator, field)
}
if !validation.IsValidDate(value) {
return FilterParam{}, fmt.Errorf("Invalid date format: %s. Expected: YYYY-MM-DD", value)
}
} else {
return FilterParam{}, fmt.Errorf("Unsupported filter field: %s", field)
}
return FilterParam{Field: field, Operator: operator, Value: value}, nil
}
And this is how the PortfolioQueryParams
struct looks like:
type PortfolioQueryParams struct {
PaginationParams
Filters []string `query:"filters,explode" doc:"Filter portfolio by various fields"`
}
This is how adding PortfolioQueryParams
to the route looks like (note that the Operation code itself, including the filter description, is under getAllPropertyOperation
- I didn't paste the complete code for that, but hopefully you get the gist of it). If validation fails, it will throw a 422 response. I also added how we can loop through the filter values that got passed:
huma.Register(api, getAllPropertyOperation(schema, "get-properties", "/properties", []string{"Properties"}),
func(ctx context.Context, input *struct {
models.Headers
models.PortfolioQueryParams
}) (*models.MultiplePropertyOutput, error) {
for _, filter := range input.Filters{
fmt.Println(filter)
}
return mockMultiplePropertyResponse(), err
})
}
I hope this helps someone. Let me know in the comments, if you found a better solution.
Top comments (0)