diff --git a/pkg/common/adapters/database/bun.go b/pkg/common/adapters/database/bun.go index aeed1b5..edc6fad 100644 --- a/pkg/common/adapters/database/bun.go +++ b/pkg/common/adapters/database/bun.go @@ -9,6 +9,7 @@ import ( "github.com/uptrace/bun" "github.com/bitechdev/ResolveSpec/pkg/common" + "github.com/bitechdev/ResolveSpec/pkg/modelregistry" "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 { 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 } diff --git a/pkg/common/adapters/database/gorm.go b/pkg/common/adapters/database/gorm.go index 1cee1ab..fb0d74d 100644 --- a/pkg/common/adapters/database/gorm.go +++ b/pkg/common/adapters/database/gorm.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" "github.com/bitechdev/ResolveSpec/pkg/common" + "github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/reflection" ) @@ -98,6 +99,7 @@ func (g *GormSelectQuery) Table(table string) common.SelectQuery { g.db = g.db.Table(table) // Check if the table name contains schema (e.g., "schema.table") g.schema, g.tableName = parseTableName(table) + return g } @@ -340,6 +342,13 @@ func (g *GormUpdateQuery) Model(model interface{}) common.UpdateQuery { func (g *GormUpdateQuery) Table(table string) common.UpdateQuery { 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 } diff --git a/pkg/common/recursive_crud.go b/pkg/common/recursive_crud.go index b819441..688da6f 100644 --- a/pkg/common/recursive_crud.go +++ b/pkg/common/recursive_crud.go @@ -378,8 +378,16 @@ func (p *NestedCUDProcessor) getTableNameForModel(model interface{}, defaultName } // 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 { + 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 if _, hasCRUDRequest := data["_request"]; hasCRUDRequest { return true @@ -407,19 +415,33 @@ func ShouldUseNestedProcessor(data map[string]interface{}, model interface{}, re if relInfo != nil { // Check if the value is actually nested data (object or array) switch v := value.(type) { - case map[string]interface{}: - //logger.Debug("Found nested relation field: %s", key) - return ShouldUseNestedProcessor(v, relInfo.RelatedModel, relationshipHelper) - case []interface{}, []map[string]interface{}: - //logger.Debug("Found nested relation field: %s", key) - for _, item := range v.([]interface{}) { - if itemMap, ok := item.(map[string]interface{}); ok { - if ShouldUseNestedProcessor(itemMap, relInfo.RelatedModel, relationshipHelper) { + case map[string]interface{}, []interface{}, []map[string]interface{}: + // If we're already at a nested level (depth > 0) and found a relation, + // that means we have multi-level nesting, so return true + if depth > 0 { + return true + } + // At depth 0, recurse to check if the nested data has further nesting + 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 false } } } diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index 32c6e90..e4330ad 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -811,7 +811,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id dataMap["id"] = targetID // Create update query - query := tx.NewUpdate().Table(tableName).SetMap(dataMap) + query := tx.NewUpdate().Model(model).Table(tableName).SetMap(dataMap) pkName := reflection.GetPrimaryKeyName(model) 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 -// 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 { 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 if field.Type.Kind() == reflect.Slice { 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 { 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") { info.relationType = "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 diff --git a/pkg/restheadspec/handler_nested_test.go b/pkg/restheadspec/handler_nested_test.go index 943ee3a..cdab932 100644 --- a/pkg/restheadspec/handler_nested_test.go +++ b/pkg/restheadspec/handler_nested_test.go @@ -132,7 +132,7 @@ func TestShouldUseNestedProcessor(t *testing.T) { expected bool }{ { - name: "Data with nested posts", + name: "Data with simple nested posts (no further nesting)", data: map[string]interface{}{ "name": "John", "posts": []map[string]interface{}{ @@ -140,7 +140,23 @@ func TestShouldUseNestedProcessor(t *testing.T) { }, }, 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", @@ -159,6 +175,20 @@ func TestShouldUseNestedProcessor(t *testing.T) { model: TestUser{}, 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 {