mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-09-30 12:01:59 +00:00
fix(json-columns): skip JSON select columns with no model scan target
Tests / Unit Tests (push) Failing after 28s
Tests / Integration Tests (push) Failing after 29s
Build , Vet Test, and Lint / Build (push) Successful in 1m12s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m43s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 1m46s
Build , Vet Test, and Lint / Lint Code (push) Successful in 1m48s
Tests / Unit Tests (push) Failing after 28s
Tests / Integration Tests (push) Failing after 29s
Build , Vet Test, and Lint / Build (push) Successful in 1m12s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m43s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 1m46s
Build , Vet Test, and Lint / Lint Code (push) Successful in 1m48s
Requesting a JSON sub-field (e.g. jsonvalue->'product'->>'cost') that has no matching bun scanonly field on the model made the whole read fail with "bun: ModelX does not have column Y", since bun scans SELECT results straight into the typed model struct. Add reflection.HasColumn to check whether the model can actually receive a given column (including scanonly fields, walking embedded structs), and gate the JSON select-column expression on it in ApplySelectColumns (shared by websocketspec/mqttspec) and the resolvespec/restheadspec handlers. When there's no scan target, drop just that column with a warning instead of erroring the whole request.
This commit is contained in:
@@ -439,6 +439,63 @@ func GetSQLModelColumns(model any) []string {
|
||||
return columns
|
||||
}
|
||||
|
||||
// HasColumn reports whether the model has a struct field that bun/gorm would
|
||||
// scan a column named columnName into. Unlike GetSQLModelColumns, this
|
||||
// includes scanonly fields (e.g. a `bun:"jsonvalue_product_cost,scanonly"`
|
||||
// field added specifically to receive a computed/JSON-path SELECT expression)
|
||||
// since those are legitimate scan targets even though they are not writable.
|
||||
// Matching is case-insensitive against the resolved bun/gorm/json column name
|
||||
// and against the bare Go field name.
|
||||
func HasColumn(model any, columnName string) bool {
|
||||
if columnName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
modelType := reflect.TypeOf(model)
|
||||
for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
if modelType == nil || modelType.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasColumnInType(modelType, columnName)
|
||||
}
|
||||
|
||||
func hasColumnInType(typ reflect.Type, columnName string) bool {
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
bunTag := field.Tag.Get("bun")
|
||||
gormTag := field.Tag.Get("gorm")
|
||||
|
||||
if field.Anonymous {
|
||||
fieldType := field.Type
|
||||
if fieldType.Kind() == reflect.Pointer {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
if hasColumnInType(fieldType, columnName) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if bunTag == "-" || gormTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(getColumnNameFromField(field), columnName) || strings.EqualFold(field.Name, columnName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// collectSQLColumnsFromType recursively collects SQL column names from a struct type
|
||||
// scanOnlyEmbedded indicates if we're inside a scan-only embedded struct
|
||||
func collectSQLColumnsFromType(typ reflect.Type, columns *[]string, scanOnlyEmbedded bool) {
|
||||
|
||||
Reference in New Issue
Block a user