Compare commits

...

6 Commits

Author SHA1 Message Date
Hein 69cc3e2839 fix(db): update Returning method to accept multiple columns 2026-05-27 14:11:20 +02:00
Hein 4018af0636 fix(validation): enhance filter logic for column validation
* adjust handling of "all" filter to consider filtered columns
fix(function_api): improve variable substitution in SQL queries
* add safeSubstituteVar for context-aware value sanitization
2026-05-27 12:17:31 +02:00
Hein c4e79d6950 fix(validation): use strings.EqualFold for case-insensitive comparison 2026-05-27 12:07:08 +02:00
Hein 982a0e62ac fix(validation): add Columns method to retrieve valid column names 2026-05-27 12:06:46 +02:00
Hein 5d459c95a7 fix(headers): reorder import statements for clarity 2026-05-27 11:28:39 +02:00
Hein e9f7726e43 fix(headers): sort combined parameters before processing 2026-05-27 11:28:22 +02:00
5 changed files with 76 additions and 9 deletions
+2 -2
View File
@@ -1489,7 +1489,7 @@ func (b *BunInsertQuery) OnConflict(action string) common.InsertQuery {
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
b.query = b.query.Returning(strings.Join(columns, ", "))
}
return b
}
@@ -1606,7 +1606,7 @@ func (b *BunUpdateQuery) Where(query string, args ...interface{}) common.UpdateQ
func (b *BunUpdateQuery) Returning(columns ...string) common.UpdateQuery {
if len(columns) > 0 {
b.query = b.query.Returning(columns[0])
b.query = b.query.Returning(strings.Join(columns, ", "))
}
return b
}
+25 -2
View File
@@ -3,6 +3,7 @@ package common
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/logger"
@@ -43,7 +44,7 @@ func (v *ColumnValidator) buildValidColumns() {
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
if !field.IsExported() {
if !field.IsExported() || field.Anonymous {
continue
}
@@ -125,6 +126,16 @@ func (v *ColumnValidator) IsValidColumn(column string) bool {
return v.ValidateColumn(column) == nil
}
// Columns returns all valid column names known to this validator
func (v *ColumnValidator) Columns() []string {
cols := make([]string, 0, len(v.validColumns))
for col := range v.validColumns {
cols = append(cols, col)
}
sort.Strings(cols)
return cols
}
// FilterValidColumns filters a list of columns, returning only valid ones
// Logs warnings for any invalid columns
func (v *ColumnValidator) FilterValidColumns(columns []string) []string {
@@ -224,7 +235,19 @@ func (v *ColumnValidator) FilterRequestOptions(options RequestOptions) RequestOp
// Filter Filter columns
validFilters := make([]FilterOption, 0, len(options.Filters))
for _, filter := range options.Filters {
if v.IsValidColumn(filter.Column) {
if strings.EqualFold(filter.Column, "all") {
allCols := v.Columns()
if len(filtered.Columns) > 0 {
allCols = filtered.Columns
}
for _, col := range allCols {
expanded := filter
expanded.Column = col
expanded.LogicOperator = "OR"
validFilters = append(validFilters, expanded)
}
} else if v.IsValidColumn(filter.Column) {
validFilters = append(validFilters, filter)
} else {
logger.Warn("Invalid column in filter '%s' removed", filter.Column)
+33 -2
View File
@@ -174,7 +174,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
varName := kw[1 : len(kw)-1] // strip [ and ]
if val, ok := variables[varName]; ok {
if strVal := fmt.Sprintf("%v", val); strVal != "" {
sqlquery = strings.ReplaceAll(sqlquery, kw, ValidSQL(strVal, "colvalue"))
sqlquery = strings.ReplaceAll(sqlquery, kw, safeSubstituteVar(sqlquery, kw, strVal))
continue
}
}
@@ -533,7 +533,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
varName := kw[1 : len(kw)-1] // strip [ and ]
if val, ok := variables[varName]; ok {
if strVal := fmt.Sprintf("%v", val); strVal != "" {
sqlquery = strings.ReplaceAll(sqlquery, kw, ValidSQL(strVal, "colvalue"))
sqlquery = strings.ReplaceAll(sqlquery, kw, safeSubstituteVar(sqlquery, kw, strVal))
continue
}
}
@@ -1006,6 +1006,37 @@ func IsNumeric(s string) bool {
return err == nil
}
// isInsideDollarQuote reports whether the first occurrence of placeholder in sqlquery
// is immediately surrounded by dollar-sign characters (i.e. inside a $...$-quoted string).
// Dollar-quoted strings pass content through literally — no backslash processing — so
// values placed there must NOT have their backslashes escaped.
func isInsideDollarQuote(sqlquery, placeholder string) bool {
idx := strings.Index(sqlquery, placeholder)
if idx < 0 {
return false
}
endIdx := idx + len(placeholder)
charBefore := byte(0)
charAfter := byte(0)
if idx > 0 {
charBefore = sqlquery[idx-1]
}
if endIdx < len(sqlquery) {
charAfter = sqlquery[endIdx]
}
return charBefore == '$' || charAfter == '$'
}
// safeSubstituteVar returns value sanitised for the quoting context that surrounds
// placeholder in sqlquery: raw (no backslash escaping) for dollar-quoted contexts,
// ValidSQL("colvalue") escaping for everything else.
func safeSubstituteVar(sqlquery, placeholder, value string) string {
if isInsideDollarQuote(sqlquery, placeholder) {
return value
}
return ValidSQL(value, "colvalue")
}
// getReplacementForBlankParam determines the replacement value for an unused parameter
// based on whether it appears within quotes in the SQL query.
// It checks for PostgreSQL quotes: single quotes (”) and dollar quotes ($...$)
+2 -2
View File
@@ -1218,8 +1218,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
if provider, ok := modelValue.(common.TableNameProvider); !ok || provider.TableName() == "" {
query = query.Table(tableName)
}
query = query.Returning("*")
fields := reflection.GetSQLModelColumns(model)
query = query.Returning(fields...)
// Execute BeforeScan hooks - pass query chain so hooks can modify it
itemHookCtx := &HookContext{
+14 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
@@ -140,9 +141,21 @@ func (h *Handler) parseOptionsFromHeaders(r common.Request, model interface{}) E
combinedParams[strings.ToLower(key)] = value
}
sortedKeys := make([]string, 0, len(combinedParams))
for key := range combinedParams {
sortedKeys = append(sortedKeys, key)
}
sort.Slice(sortedKeys, func(i, j int) bool {
if sortedKeys[i] != sortedKeys[j] {
return sortedKeys[i] < sortedKeys[j]
}
return combinedParams[sortedKeys[i]] < combinedParams[sortedKeys[j]]
})
// Process each parameter (from both headers and query params)
// Note: keys are already normalized to lowercase in combinedParams
for key, value := range combinedParams {
for _, key := range sortedKeys {
value := combinedParams[key]
// Decode value if it's base64 encoded
decodedValue := decodeHeaderValue(value)