A library to parse and validate advanced REST API query parameters including filters, pagination, sorting, grouping, and search with logical operators.
RQL takes a Golang struct and a JSON string as input and returns a validated object that can be used to prepare SQL statements using raw SQL or ORM query builders.
Installation
go get github.com/raystack/salt/rql
Features
- Filter Support: Complex filtering with operators (eq, neq, like, gt, lt, etc.)
- Pagination: Offset and limit support
- Sorting: Multi-field sorting with asc/desc ordering
- Grouping: Group by multiple fields
- Search: Fuzzy search across specified columns
- Type Validation: Validates data types using struct tags
- Operator Validation: Ensures operators are valid for each data type
Data Structures
Query
Represents a complete query with filters, sorting, pagination, and more.
type Query struct {
Filters []Filter `json:"filters"`
GroupBy []string `json:"group_by"`
Offset int `json:"offset"`
Limit int `json:"limit"`
Search string `json:"search"`
Sort []Sort `json:"sort"`
}
Array of filter conditions to apply
Fields to group results by
Number of records to skip (for pagination)
Maximum number of records to return
Search term for fuzzy search across columns
Filter
Represents a single filter condition.
type Filter struct {
Name string `json:"name"`
Operator string `json:"operator"`
Value any `json:"value"`
}
Field name to filter on (must match struct tag)
Comparison operator (e.g., eq, neq, like, gt, gte, lt, lte, in, notin)
Value to compare against (type must match field data type)
Sort
Represents sorting criteria for a field.
type Sort struct {
Name string `json:"name"`
Order string `json:"order"`
}
Sort order: "asc" or "desc"
Functions
ValidateQuery
Validates a query against a struct definition with RQL tags.
func ValidateQuery(q *Query, checkStruct interface{}) error
A struct with rql tags defining valid fields and types
Validation error if query is invalid, nil if valid
Example:
import (
"encoding/json"
"github.com/raystack/salt/rql"
)
type Organization struct {
Id int `rql:"name=id,type=number,min=10,max=200"`
BillingPlanName string `rql:"name=plan_name,type=string"`
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
MemberCount int `rql:"name=member_count,type=number"`
Title string `rql:"name=title,type=string"`
Enabled bool `rql:"name=enabled,type=bool"`
}
userInput := &rql.Query{}
err := json.Unmarshal(jsonBytes, userInput)
if err != nil {
panic(err)
}
err = rql.ValidateQuery(userInput, Organization{})
if err != nil {
panic(err)
}
GetDataTypeOfField
Retrieves the data type of a field from the struct definition.
func GetDataTypeOfField(fieldName string, checkStruct interface{}) (string, error)
Name of the field to check
The data type: "number", "string", "datetime", or "bool"
Error if field not found or invalid data type
Supported Data Types
Define data types using the type parameter in RQL tags:
Numeric values (int, uint, float, etc.)Operators: eq, neq, gt, lt, gte, lte
String valuesOperators: eq, neq, like, in, notin, notlike, empty, notempty
ISO 8601 datetime strings (RFC3339 format)Operators: eq, neq, gt, lt, gte, lte
Boolean valuesOperators: eq, neq
Define validation rules using the rql struct tag:
`rql:"name=field_name,type=datatype,min=value,max=value"`
Field name as used in queries (defaults to struct field name)
Data type: number, string, datetime, or bool
Minimum value (for number types) - planned feature
Maximum value (for number types) - planned feature
Constants
const TAG = "rql"
const DATATYPE_NUMBER = "number"
const DATATYPE_DATETIME = "datetime"
const DATATYPE_STRING = "string"
const DATATYPE_BOOL = "bool"
const SORT_ORDER_ASC = "asc"
const SORT_ORDER_DESC = "desc"
Frontend should send parameters in this JSON schema:
{
"filters": [
{ "name": "id", "operator": "neq", "value": 20 },
{ "name": "title", "operator": "like", "value": "xyz" },
{ "name": "enabled", "operator": "eq", "value": false },
{
"name": "created_at",
"operator": "gte",
"value": "2025-02-05T11:25:37.957Z"
}
],
"group_by": ["plan_name"],
"offset": 20,
"limit": 50,
"search": "abcd",
"sort": [
{ "name": "title", "order": "desc" },
{ "name": "created_at", "order": "asc" }
]
}
Complete Example with SQL Generation
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/raystack/salt/rql"
)
type Organization struct {
Id int `rql:"name=id,type=number,min=10,max=200"`
BillingPlanName string `rql:"name=plan_name,type=string"`
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
MemberCount int `rql:"name=member_count,type=number"`
Title string `rql:"name=title,type=string"`
Enabled bool `rql:"name=enabled,type=bool"`
}
func main() {
queryJSON := `{
"filters": [
{"name": "id", "operator": "neq", "value": 20},
{"name": "enabled", "operator": "eq", "value": false}
],
"offset": 20,
"limit": 50,
"search": "abcd",
"sort": [
{"name": "title", "order": "desc"}
]
}`
// Parse query
userInput := &rql.Query{}
err := json.Unmarshal([]byte(queryJSON), userInput)
if err != nil {
panic(err)
}
// Validate query
err = rql.ValidateQuery(userInput, Organization{})
if err != nil {
panic(err)
}
// Build SQL using goqu
query := goqu.From("organizations")
// Apply filters
for _, filter := range userInput.Filters {
query = query.Where(goqu.Ex{
filter.Name: goqu.Op{filter.Operator: filter.Value},
})
}
// Apply fuzzy search
fuzzySearchColumns := []string{"id", "billing_plan_name", "title"}
if userInput.Search != "" {
expressions := make([]goqu.Expression, 0)
for _, col := range fuzzySearchColumns {
expressions = append(expressions, goqu.Ex{
col: goqu.Op{"LIKE": userInput.Search},
})
}
query = query.Where(goqu.Or(expressions...))
}
// Apply sorting
for _, sort := range userInput.Sort {
switch sort.Order {
case "asc":
query = query.OrderAppend(goqu.C(sort.Name).Asc())
case "desc":
query = query.OrderAppend(goqu.C(sort.Name).Desc())
}
}
// Apply pagination
query = query.Offset(uint(userInput.Offset))
query = query.Limit(uint(userInput.Limit))
sql, _, _ := query.ToSQL()
fmt.Println(sql)
}
Output:
SELECT * FROM "organizations"
WHERE (("id" != 20) AND ("enabled" IS FALSE)
AND (("id" LIKE 'abcd') OR ("billing_plan_name" LIKE 'abcd') OR ("title" LIKE 'abcd')))
ORDER BY "title" DESC
LIMIT 50 OFFSET 20
Error Handling
The library returns detailed validation errors for:
- Invalid field names that don’t exist in the struct
- Invalid operators for the field’s data type
- Type mismatches between filter values and field types
- Invalid datetime formats (must be RFC3339)
- Invalid sort orders (must be “asc” or “desc”)
Future Improvements
- Operator mapping to SQL operators (currently relies on query builders like GoQU)
- Support for
min and max validation on numeric values
- More complex query expressions with AND/OR grouping