Fixes on getRelationshipInfo, ShouldUseNestedProcessor

This commit is contained in:
Hein 2025-11-19 18:03:25 +02:00
parent 8b7db5b31a
commit a44ef90d7c
5 changed files with 113 additions and 14 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
"github.com/bitechdev/ResolveSpec/pkg/reflection" "github.com/bitechdev/ResolveSpec/pkg/reflection"
) )
@ -365,6 +366,14 @@ func (b *BunUpdateQuery) Model(model interface{}) common.UpdateQuery {
func (b *BunUpdateQuery) Table(table string) common.UpdateQuery { func (b *BunUpdateQuery) Table(table string) common.UpdateQuery {
b.query = b.query.Table(table) b.query = b.query.Table(table)
if b.model == nil {
// Try to get table name from table string if model is not set
model, err := modelregistry.GetModelByName(table)
if err == nil {
b.model = model
}
}
return b return b
} }

View File

@ -8,6 +8,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
"github.com/bitechdev/ResolveSpec/pkg/reflection" "github.com/bitechdev/ResolveSpec/pkg/reflection"
) )
@ -98,6 +99,7 @@ func (g *GormSelectQuery) Table(table string) common.SelectQuery {
g.db = g.db.Table(table) g.db = g.db.Table(table)
// Check if the table name contains schema (e.g., "schema.table") // Check if the table name contains schema (e.g., "schema.table")
g.schema, g.tableName = parseTableName(table) g.schema, g.tableName = parseTableName(table)
return g return g
} }
@ -340,6 +342,13 @@ func (g *GormUpdateQuery) Model(model interface{}) common.UpdateQuery {
func (g *GormUpdateQuery) Table(table string) common.UpdateQuery { func (g *GormUpdateQuery) Table(table string) common.UpdateQuery {
g.db = g.db.Table(table) g.db = g.db.Table(table)
if g.model == nil {
// Try to get table name from table string if model is not set
model, err := modelregistry.GetModelByName(table)
if err == nil {
g.model = model
}
}
return g return g
} }

View File

@ -378,8 +378,16 @@ func (p *NestedCUDProcessor) getTableNameForModel(model interface{}, defaultName
} }
// ShouldUseNestedProcessor determines if we should use nested CUD processing // ShouldUseNestedProcessor determines if we should use nested CUD processing
// It checks if the data contains nested relations or a _request field // It recursively checks if the data contains:
// 1. A _request field at any level, OR
// 2. Nested relations that themselves contain further nested relations or _request fields
// This ensures nested processing is only used when there are deeply nested operations
func ShouldUseNestedProcessor(data map[string]interface{}, model interface{}, relationshipHelper RelationshipInfoProvider) bool { func ShouldUseNestedProcessor(data map[string]interface{}, model interface{}, relationshipHelper RelationshipInfoProvider) bool {
return shouldUseNestedProcessorDepth(data, model, relationshipHelper, 0)
}
// shouldUseNestedProcessorDepth is the internal recursive implementation with depth tracking
func shouldUseNestedProcessorDepth(data map[string]interface{}, model interface{}, relationshipHelper RelationshipInfoProvider, depth int) bool {
// Check for _request field // Check for _request field
if _, hasCRUDRequest := data["_request"]; hasCRUDRequest { if _, hasCRUDRequest := data["_request"]; hasCRUDRequest {
return true return true
@ -407,19 +415,33 @@ func ShouldUseNestedProcessor(data map[string]interface{}, model interface{}, re
if relInfo != nil { if relInfo != nil {
// Check if the value is actually nested data (object or array) // Check if the value is actually nested data (object or array)
switch v := value.(type) { switch v := value.(type) {
case map[string]interface{}: case map[string]interface{}, []interface{}, []map[string]interface{}:
//logger.Debug("Found nested relation field: %s", key) // If we're already at a nested level (depth > 0) and found a relation,
return ShouldUseNestedProcessor(v, relInfo.RelatedModel, relationshipHelper) // that means we have multi-level nesting, so return true
case []interface{}, []map[string]interface{}: if depth > 0 {
//logger.Debug("Found nested relation field: %s", key) return true
for _, item := range v.([]interface{}) { }
if itemMap, ok := item.(map[string]interface{}); ok { // At depth 0, recurse to check if the nested data has further nesting
if ShouldUseNestedProcessor(itemMap, relInfo.RelatedModel, relationshipHelper) { switch typedValue := v.(type) {
case map[string]interface{}:
if shouldUseNestedProcessorDepth(typedValue, relInfo.RelatedModel, relationshipHelper, depth+1) {
return true
}
case []interface{}:
for _, item := range typedValue {
if itemMap, ok := item.(map[string]interface{}); ok {
if shouldUseNestedProcessorDepth(itemMap, relInfo.RelatedModel, relationshipHelper, depth+1) {
return true
}
}
}
case []map[string]interface{}:
for _, itemMap := range typedValue {
if shouldUseNestedProcessorDepth(itemMap, relInfo.RelatedModel, relationshipHelper, depth+1) {
return true return true
} }
} }
} }
return false
} }
} }
} }

View File

@ -811,7 +811,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
dataMap["id"] = targetID dataMap["id"] = targetID
// Create update query // Create update query
query := tx.NewUpdate().Table(tableName).SetMap(dataMap) query := tx.NewUpdate().Model(model).Table(tableName).SetMap(dataMap)
pkName := reflection.GetPrimaryKeyName(model) pkName := reflection.GetPrimaryKeyName(model)
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID) query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
@ -1904,7 +1904,8 @@ func filterExtendedOptions(validator *common.ColumnValidator, options ExtendedRe
} }
// shouldUseNestedProcessor determines if we should use nested CUD processing // shouldUseNestedProcessor determines if we should use nested CUD processing
// It checks if the data contains nested relations or a _request field // It recursively checks if the data contains deeply nested relations or _request fields
// Simple one-level relations without further nesting don't require the nested processor
func (h *Handler) shouldUseNestedProcessor(data map[string]interface{}, model interface{}) bool { func (h *Handler) shouldUseNestedProcessor(data map[string]interface{}, model interface{}) bool {
return common.ShouldUseNestedProcessor(data, model, h) return common.ShouldUseNestedProcessor(data, model, h)
} }
@ -1966,12 +1967,40 @@ func (h *Handler) getRelationshipInfo(modelType reflect.Type, relationName strin
// Determine if it's belongsTo or hasMany/hasOne // Determine if it's belongsTo or hasMany/hasOne
if field.Type.Kind() == reflect.Slice { if field.Type.Kind() == reflect.Slice {
info.relationType = "hasMany" info.relationType = "hasMany"
// Get the element type for slice
elemType := field.Type.Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
if elemType.Kind() == reflect.Struct {
info.relatedModel = reflect.New(elemType).Elem().Interface()
}
} else if field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct { } else if field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct {
info.relationType = "belongsTo" info.relationType = "belongsTo"
elemType := field.Type
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
if elemType.Kind() == reflect.Struct {
info.relatedModel = reflect.New(elemType).Elem().Interface()
}
} }
} else if strings.Contains(gormTag, "many2many") { } else if strings.Contains(gormTag, "many2many") {
info.relationType = "many2many" info.relationType = "many2many"
info.joinTable = h.extractTagValue(gormTag, "many2many") info.joinTable = h.extractTagValue(gormTag, "many2many")
// Get the element type for many2many (always slice)
if field.Type.Kind() == reflect.Slice {
elemType := field.Type.Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
if elemType.Kind() == reflect.Struct {
info.relatedModel = reflect.New(elemType).Elem().Interface()
}
}
} else {
// Field has no GORM relationship tags, so it's not a relation
return nil
} }
return info return info

View File

@ -132,7 +132,7 @@ func TestShouldUseNestedProcessor(t *testing.T) {
expected bool expected bool
}{ }{
{ {
name: "Data with nested posts", name: "Data with simple nested posts (no further nesting)",
data: map[string]interface{}{ data: map[string]interface{}{
"name": "John", "name": "John",
"posts": []map[string]interface{}{ "posts": []map[string]interface{}{
@ -140,7 +140,23 @@ func TestShouldUseNestedProcessor(t *testing.T) {
}, },
}, },
model: TestUser{}, model: TestUser{},
expected: true, expected: false, // Simple one-level nesting doesn't require nested processor
},
{
name: "Data with deeply nested relations",
data: map[string]interface{}{
"name": "John",
"posts": []map[string]interface{}{
{
"title": "Post 1",
"comments": []map[string]interface{}{
{"content": "Comment 1"},
},
},
},
},
model: TestUser{},
expected: true, // Multi-level nesting requires nested processor
}, },
{ {
name: "Data without nested relations", name: "Data without nested relations",
@ -159,6 +175,20 @@ func TestShouldUseNestedProcessor(t *testing.T) {
model: TestUser{}, model: TestUser{},
expected: true, expected: true,
}, },
{
name: "Nested data with _request field",
data: map[string]interface{}{
"name": "John",
"posts": []map[string]interface{}{
{
"_request": "insert",
"title": "Post 1",
},
},
},
model: TestUser{},
expected: true, // _request at nested level requires nested processor
},
} }
for _, tt := range tests { for _, tt := range tests {