mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-04 12:53:45 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 938a2ef3d9 | |||
| 69cc3e2839 | |||
| 4018af0636 | |||
| c4e79d6950 | |||
| 982a0e62ac | |||
| 5d459c95a7 | |||
| e9f7726e43 |
@@ -1489,7 +1489,7 @@ func (b *BunInsertQuery) OnConflict(action string) common.InsertQuery {
|
|||||||
|
|
||||||
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
|
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
if len(columns) > 0 {
|
if len(columns) > 0 {
|
||||||
b.query = b.query.Returning(columns[0])
|
b.query = b.query.Returning(strings.Join(columns, ", "))
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -1606,7 +1606,7 @@ func (b *BunUpdateQuery) Where(query string, args ...interface{}) common.UpdateQ
|
|||||||
|
|
||||||
func (b *BunUpdateQuery) Returning(columns ...string) common.UpdateQuery {
|
func (b *BunUpdateQuery) Returning(columns ...string) common.UpdateQuery {
|
||||||
if len(columns) > 0 {
|
if len(columns) > 0 {
|
||||||
b.query = b.query.Returning(columns[0])
|
b.query = b.query.Returning(strings.Join(columns, ", "))
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package common
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
@@ -43,7 +44,7 @@ func (v *ColumnValidator) buildValidColumns() {
|
|||||||
for i := 0; i < modelType.NumField(); i++ {
|
for i := 0; i < modelType.NumField(); i++ {
|
||||||
field := modelType.Field(i)
|
field := modelType.Field(i)
|
||||||
|
|
||||||
if !field.IsExported() {
|
if !field.IsExported() || field.Anonymous {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +126,16 @@ func (v *ColumnValidator) IsValidColumn(column string) bool {
|
|||||||
return v.ValidateColumn(column) == nil
|
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
|
// FilterValidColumns filters a list of columns, returning only valid ones
|
||||||
// Logs warnings for any invalid columns
|
// Logs warnings for any invalid columns
|
||||||
func (v *ColumnValidator) FilterValidColumns(columns []string) []string {
|
func (v *ColumnValidator) FilterValidColumns(columns []string) []string {
|
||||||
@@ -224,7 +235,19 @@ func (v *ColumnValidator) FilterRequestOptions(options RequestOptions) RequestOp
|
|||||||
// Filter Filter columns
|
// Filter Filter columns
|
||||||
validFilters := make([]FilterOption, 0, len(options.Filters))
|
validFilters := make([]FilterOption, 0, len(options.Filters))
|
||||||
for _, filter := range 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)
|
validFilters = append(validFilters, filter)
|
||||||
} else {
|
} else {
|
||||||
logger.Warn("Invalid column in filter '%s' removed", filter.Column)
|
logger.Warn("Invalid column in filter '%s' removed", filter.Column)
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
varName := kw[1 : len(kw)-1] // strip [ and ]
|
varName := kw[1 : len(kw)-1] // strip [ and ]
|
||||||
if val, ok := variables[varName]; ok {
|
if val, ok := variables[varName]; ok {
|
||||||
if strVal := fmt.Sprintf("%v", val); strVal != "" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +533,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
varName := kw[1 : len(kw)-1] // strip [ and ]
|
varName := kw[1 : len(kw)-1] // strip [ and ]
|
||||||
if val, ok := variables[varName]; ok {
|
if val, ok := variables[varName]; ok {
|
||||||
if strVal := fmt.Sprintf("%v", val); strVal != "" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,6 +1006,37 @@ func IsNumeric(s string) bool {
|
|||||||
return err == nil
|
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
|
// getReplacementForBlankParam determines the replacement value for an unused parameter
|
||||||
// based on whether it appears within quotes in the SQL query.
|
// based on whether it appears within quotes in the SQL query.
|
||||||
// It checks for PostgreSQL quotes: single quotes (”) and dollar quotes ($...$)
|
// It checks for PostgreSQL quotes: single quotes (”) and dollar quotes ($...$)
|
||||||
|
|||||||
@@ -1218,8 +1218,8 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if provider, ok := modelValue.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
if provider, ok := modelValue.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
||||||
query = query.Table(tableName)
|
query = query.Table(tableName)
|
||||||
}
|
}
|
||||||
|
fields := reflection.GetSQLModelColumns(model)
|
||||||
query = query.Returning("*")
|
query = query.Returning(fields...)
|
||||||
|
|
||||||
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
||||||
itemHookCtx := &HookContext{
|
itemHookCtx := &HookContext{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -140,9 +141,21 @@ func (h *Handler) parseOptionsFromHeaders(r common.Request, model interface{}) E
|
|||||||
combinedParams[strings.ToLower(key)] = value
|
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)
|
// Process each parameter (from both headers and query params)
|
||||||
// Note: keys are already normalized to lowercase in combinedParams
|
// 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
|
// Decode value if it's base64 encoded
|
||||||
decodedValue := decodeHeaderValue(value)
|
decodedValue := decodeHeaderValue(value)
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,25 @@ func (m *mountPoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Try to open the file
|
// Try to open the file
|
||||||
file, err := m.provider.Open(strings.TrimPrefix(filePath, "/"))
|
file, err := m.provider.Open(strings.TrimPrefix(filePath, "/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// For extensionless paths, also try path/index.html
|
||||||
|
if path.Ext(filePath) == "" {
|
||||||
|
indexFallback := path.Join(filePath, "index.html")
|
||||||
|
file, err = m.provider.Open(strings.TrimPrefix(indexFallback, "/"))
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
m.serveFile(w, r, indexFallback, file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexFallback = fmt.Sprintf("%s.html", filePath)
|
||||||
|
file, err = m.provider.Open(strings.TrimPrefix(indexFallback, "/"))
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
m.serveFile(w, r, indexFallback, file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File doesn't exist - check if we should use fallback
|
// File doesn't exist - check if we should use fallback
|
||||||
if m.fallbackStrategy != nil && m.fallbackStrategy.ShouldFallback(filePath) {
|
if m.fallbackStrategy != nil && m.fallbackStrategy.ShouldFallback(filePath) {
|
||||||
fallbackPath := m.fallbackStrategy.GetFallbackPath(filePath)
|
fallbackPath := m.fallbackStrategy.GetFallbackPath(filePath)
|
||||||
@@ -80,16 +99,6 @@ func (m *mountPoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For extensionless paths, also try path/index.html
|
|
||||||
if path.Ext(filePath) == "" {
|
|
||||||
indexFallback := path.Join(filePath, "index.html")
|
|
||||||
file, err = m.provider.Open(strings.TrimPrefix(indexFallback, "/"))
|
|
||||||
if err == nil {
|
|
||||||
defer file.Close()
|
|
||||||
m.serveFile(w, r, indexFallback, file)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No fallback or fallback failed - return 404
|
// No fallback or fallback failed - return 404
|
||||||
|
|||||||
Reference in New Issue
Block a user