Compare commits

..

4 Commits

Author SHA1 Message Date
d1ae4fe64e refactor(handler): unify filter operator handling for consistency
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -30m26s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -29m58s
Build , Vet Test, and Lint / Lint Code (push) Successful in -29m48s
Build , Vet Test, and Lint / Build (push) Successful in -30m4s
Tests / Integration Tests (push) Failing after -30m39s
Tests / Unit Tests (push) Successful in -30m29s
2026-03-01 13:21:38 +02:00
254102bfac refactor(auth): simplify handler type assertions for middleware
Some checks failed
Build , Vet Test, and Lint / Lint Code (push) Successful in -29m19s
Build , Vet Test, and Lint / Build (push) Successful in -29m42s
Tests / Integration Tests (push) Failing after -30m35s
Tests / Unit Tests (push) Successful in -30m17s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -30m2s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -29m31s
2026-03-01 12:08:36 +02:00
6c27419dbc refactor(auth): enhance request handling with middleware-enriched context 2026-03-01 12:06:43 +02:00
377336caf4 feat(sql): implement IN condition handling with parameterized queries 2026-03-01 09:52:32 +02:00
7 changed files with 100 additions and 48 deletions

View File

@@ -2,6 +2,7 @@ package common
import (
"fmt"
"reflect"
"regexp"
"strings"
@@ -925,3 +926,36 @@ func extractLeftSideOfComparison(cond string) string {
return ""
}
// FilterValueToSlice converts a filter value to []interface{} for use with IN operators.
// JSON-decoded arrays arrive as []interface{}, but typed slices (e.g. []string) also work.
// Returns a single-element slice if the value is not a slice type.
func FilterValueToSlice(v interface{}) []interface{} {
if v == nil {
return nil
}
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
result := make([]interface{}, rv.Len())
for i := 0; i < rv.Len(); i++ {
result[i] = rv.Index(i).Interface()
}
return result
}
return []interface{}{v}
}
// BuildInCondition builds a parameterized IN condition from a filter value.
// Returns the condition string (e.g. "col IN (?,?)") and the individual values as args.
// Returns ("", nil) if the value is empty or not a slice.
func BuildInCondition(column string, v interface{}) (query string, args []interface{}) {
values := FilterValueToSlice(v)
if len(values) == 0 {
return "", nil
}
placeholders := make([]string, len(values))
for i := range values {
placeholders[i] = "?"
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ",")), values
}

View File

@@ -44,8 +44,8 @@ func TestBuildFilterCondition(t *testing.T) {
Operator: "in",
Value: []string{"active", "pending"},
},
expectedCondition: "status IN (?)",
expectedArgsCount: 1,
expectedCondition: "status IN (?,?)",
expectedArgsCount: 2,
},
{
name: "LIKE operator",

View File

@@ -1521,22 +1521,22 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
var args []interface{}
switch filter.Operator {
case "eq":
case "eq", "=":
condition = fmt.Sprintf("%s = ?", filter.Column)
args = []interface{}{filter.Value}
case "neq":
case "neq", "!=", "<>":
condition = fmt.Sprintf("%s != ?", filter.Column)
args = []interface{}{filter.Value}
case "gt":
case "gt", ">":
condition = fmt.Sprintf("%s > ?", filter.Column)
args = []interface{}{filter.Value}
case "gte":
case "gte", ">=":
condition = fmt.Sprintf("%s >= ?", filter.Column)
args = []interface{}{filter.Value}
case "lt":
case "lt", "<":
condition = fmt.Sprintf("%s < ?", filter.Column)
args = []interface{}{filter.Value}
case "lte":
case "lte", "<=":
condition = fmt.Sprintf("%s <= ?", filter.Column)
args = []interface{}{filter.Value}
case "like":
@@ -1546,8 +1546,10 @@ func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionStr
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "in":
condition = fmt.Sprintf("%s IN (?)", filter.Column)
args = []interface{}{filter.Value}
condition, args = common.BuildInCondition(filter.Column, filter.Value)
if condition == "" {
return "", nil
}
default:
return "", nil
}
@@ -1563,22 +1565,22 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
var args []interface{}
switch filter.Operator {
case "eq":
case "eq", "=":
condition = fmt.Sprintf("%s = ?", filter.Column)
args = []interface{}{filter.Value}
case "neq":
case "neq", "!=", "<>":
condition = fmt.Sprintf("%s != ?", filter.Column)
args = []interface{}{filter.Value}
case "gt":
case "gt", ">":
condition = fmt.Sprintf("%s > ?", filter.Column)
args = []interface{}{filter.Value}
case "gte":
case "gte", ">=":
condition = fmt.Sprintf("%s >= ?", filter.Column)
args = []interface{}{filter.Value}
case "lt":
case "lt", "<":
condition = fmt.Sprintf("%s < ?", filter.Column)
args = []interface{}{filter.Value}
case "lte":
case "lte", "<=":
condition = fmt.Sprintf("%s <= ?", filter.Column)
args = []interface{}{filter.Value}
case "like":
@@ -1588,8 +1590,10 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
condition = fmt.Sprintf("%s ILIKE ?", filter.Column)
args = []interface{}{filter.Value}
case "in":
condition = fmt.Sprintf("%s IN (?)", filter.Column)
args = []interface{}{filter.Value}
condition, args = common.BuildInCondition(filter.Column, filter.Value)
if condition == "" {
return query
}
default:
return query
}

View File

@@ -70,17 +70,17 @@ func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware Midd
entityWithIDPath := buildRoutePath(schema, entity) + "/{id}"
// Create handler functions for this specific entity
postEntityHandler := createMuxHandler(handler, schema, entity, "")
postEntityWithIDHandler := createMuxHandler(handler, schema, entity, "id")
getEntityHandler := createMuxGetHandler(handler, schema, entity, "")
var postEntityHandler http.Handler = createMuxHandler(handler, schema, entity, "")
var postEntityWithIDHandler http.Handler = createMuxHandler(handler, schema, entity, "id")
var getEntityHandler http.Handler = createMuxGetHandler(handler, schema, entity, "")
optionsEntityHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "POST", "OPTIONS"})
optionsEntityWithIDHandler := createMuxOptionsHandler(handler, schema, entity, []string{"POST", "OPTIONS"})
// Apply authentication middleware if provided
if authMiddleware != nil {
postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc)
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc)
getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc)
postEntityHandler = authMiddleware(postEntityHandler)
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler)
getEntityHandler = authMiddleware(getEntityHandler)
// Don't apply auth middleware to OPTIONS - CORS preflight must not require auth
}
@@ -225,7 +225,11 @@ func wrapBunRouterHandler(handler bunrouter.HandlerFunc, authMiddleware Middlewa
return func(w http.ResponseWriter, req bunrouter.Request) error {
// Create an http.Handler that calls the bunrouter handler
httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = handler(w, req)
// Replace the embedded *http.Request with the middleware-enriched one
// so that auth context (user ID, etc.) is visible to the handler.
enrichedReq := req
enrichedReq.Request = r
_ = handler(w, enrichedReq)
})
// Wrap with auth middleware and execute

View File

@@ -2146,7 +2146,11 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
// Column is already cast to TEXT if needed
return applyWhere(fmt.Sprintf("%s ILIKE ?", qualifiedColumn), filter.Value)
case "in":
return applyWhere(fmt.Sprintf("%s IN (?)", qualifiedColumn), filter.Value)
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
if cond == "" {
return query
}
return applyWhere(cond, inArgs...)
case "between":
// Handle between operator - exclusive (> val1 AND < val2)
if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 {
@@ -2222,24 +2226,25 @@ func (h *Handler) applyOrFilterGroup(query common.SelectQuery, filters []*common
// buildFilterCondition builds a single filter condition and returns the condition string and args
func (h *Handler) buildFilterCondition(qualifiedColumn string, filter *common.FilterOption, tableName string) (filterStr string, filterInterface []interface{}) {
switch strings.ToLower(filter.Operator) {
case "eq", "equals":
case "eq", "equals", "=":
return fmt.Sprintf("%s = ?", qualifiedColumn), []interface{}{filter.Value}
case "neq", "not_equals", "ne":
case "neq", "not_equals", "ne", "!=", "<>":
return fmt.Sprintf("%s != ?", qualifiedColumn), []interface{}{filter.Value}
case "gt", "greater_than":
case "gt", "greater_than", ">":
return fmt.Sprintf("%s > ?", qualifiedColumn), []interface{}{filter.Value}
case "gte", "greater_than_equals", "ge":
case "gte", "greater_than_equals", "ge", ">=":
return fmt.Sprintf("%s >= ?", qualifiedColumn), []interface{}{filter.Value}
case "lt", "less_than":
case "lt", "less_than", "<":
return fmt.Sprintf("%s < ?", qualifiedColumn), []interface{}{filter.Value}
case "lte", "less_than_equals", "le":
case "lte", "less_than_equals", "le", "<=":
return fmt.Sprintf("%s <= ?", qualifiedColumn), []interface{}{filter.Value}
case "like":
return fmt.Sprintf("%s LIKE ?", qualifiedColumn), []interface{}{filter.Value}
case "ilike":
return fmt.Sprintf("%s ILIKE ?", qualifiedColumn), []interface{}{filter.Value}
case "in":
return fmt.Sprintf("%s IN (?)", qualifiedColumn), []interface{}{filter.Value}
cond, inArgs := common.BuildInCondition(qualifiedColumn, filter.Value)
return cond, inArgs
case "between":
// Handle between operator - exclusive (> val1 AND < val2)
if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 {

View File

@@ -125,17 +125,17 @@ func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware Midd
metadataPath := buildRoutePath(schema, entity) + "/metadata"
// Create handler functions for this specific entity
entityHandler := createMuxHandler(handler, schema, entity, "")
entityWithIDHandler := createMuxHandler(handler, schema, entity, "id")
metadataHandler := createMuxGetHandler(handler, schema, entity, "")
var entityHandler http.Handler = createMuxHandler(handler, schema, entity, "")
var entityWithIDHandler http.Handler = createMuxHandler(handler, schema, entity, "id")
var metadataHandler http.Handler = createMuxGetHandler(handler, schema, entity, "")
optionsEntityHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "POST", "OPTIONS"})
optionsEntityWithIDHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "PUT", "PATCH", "DELETE", "POST", "OPTIONS"})
// Apply authentication middleware if provided
if authMiddleware != nil {
entityHandler = authMiddleware(entityHandler).(http.HandlerFunc)
entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc)
metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc)
entityHandler = authMiddleware(entityHandler)
entityWithIDHandler = authMiddleware(entityWithIDHandler)
metadataHandler = authMiddleware(metadataHandler)
// Don't apply auth middleware to OPTIONS - CORS preflight must not require auth
}
@@ -289,7 +289,11 @@ func wrapBunRouterHandler(handler bunrouter.HandlerFunc, authMiddleware Middlewa
return func(w http.ResponseWriter, req bunrouter.Request) error {
// Create an http.Handler that calls the bunrouter handler
httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = handler(w, req)
// Replace the embedded *http.Request with the middleware-enriched one
// so that auth context (user ID, etc.) is visible to the handler.
enrichedReq := req
enrichedReq.Request = r
_ = handler(w, enrichedReq)
})
// Wrap with auth middleware and execute

View File

@@ -628,7 +628,10 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
countQuery := h.db.NewSelect().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
if hookCtx.Options != nil {
for _, filter := range hookCtx.Options.Filters {
countQuery = countQuery.Where(fmt.Sprintf("%s %s ?", filter.Column, h.getOperatorSQL(filter.Operator)), filter.Value)
cond, args := h.buildFilterCondition(filter)
if cond != "" {
countQuery = countQuery.Where(cond, args...)
}
}
}
count, _ := countQuery.Count(hookCtx.Context)
@@ -800,14 +803,12 @@ func (h *Handler) applyFilterGroup(query common.SelectQuery, filters []common.Fi
// buildFilterCondition builds a filter condition and returns it with args
func (h *Handler) buildFilterCondition(filter common.FilterOption) (conditionString string, conditionArgs []interface{}) {
var condition string
var args []interface{}
if strings.EqualFold(filter.Operator, "in") {
cond, args := common.BuildInCondition(filter.Column, filter.Value)
return cond, args
}
operatorSQL := h.getOperatorSQL(filter.Operator)
condition = fmt.Sprintf("%s %s ?", filter.Column, operatorSQL)
args = []interface{}{filter.Value}
return condition, args
return fmt.Sprintf("%s %s ?", filter.Column, operatorSQL), []interface{}{filter.Value}
}
// setRowNumbersOnRecords sets the RowNumber field on each record if it exists