Compare commits

..

3 Commits

Author SHA1 Message Date
Hein
456c165814 Fixed models being icorrectly set and added SetDefaultRegistry 2025-11-19 18:22:56 +02:00
Hein
850d7b546c Added modelregistry.AddRegistry 2025-11-19 18:18:18 +02:00
Hein
a44ef90d7c Fixes on getRelationshipInfo, ShouldUseNestedProcessor 2025-11-19 18:03:25 +02:00
6 changed files with 158 additions and 21 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -17,6 +17,10 @@ var defaultRegistry = &DefaultModelRegistry{
models: make(map[string]interface{}),
}
// Global list of registries (searched in order)
var registries = []*DefaultModelRegistry{defaultRegistry}
var registriesMutex sync.RWMutex
// NewModelRegistry creates a new model registry
func NewModelRegistry() *DefaultModelRegistry {
return &DefaultModelRegistry{
@@ -24,6 +28,18 @@ func NewModelRegistry() *DefaultModelRegistry {
}
}
func SetDefaultRegistry(registry *DefaultModelRegistry) {
defaultRegistry = registry
}
// AddRegistry adds a registry to the global list of registries
// Registries are searched in the order they were added
func AddRegistry(registry *DefaultModelRegistry) {
registriesMutex.Lock()
defer registriesMutex.Unlock()
registries = append(registries, registry)
}
func (r *DefaultModelRegistry) RegisterModel(name string, model interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
@@ -107,9 +123,19 @@ func RegisterModel(model interface{}, name string) error {
return defaultRegistry.RegisterModel(name, model)
}
// GetModelByName retrieves a model from the default global registry by name
// GetModelByName retrieves a model by searching through all registries in order
// Returns the first match found
func GetModelByName(name string) (interface{}, error) {
return defaultRegistry.GetModel(name)
registriesMutex.RLock()
defer registriesMutex.RUnlock()
for _, registry := range registries {
if model, err := registry.GetModel(name); err == nil {
return model, nil
}
}
return nil, fmt.Errorf("model %s not found in any registry", name)
}
// IterateModels iterates over all models in the default global registry
@@ -122,14 +148,26 @@ func IterateModels(fn func(name string, model interface{})) {
}
}
// GetModels returns a list of all models in the default global registry
// GetModels returns a list of all models from all registries
// Models are collected in registry order, with duplicates included
func GetModels() []interface{} {
defaultRegistry.mutex.RLock()
defer defaultRegistry.mutex.RUnlock()
registriesMutex.RLock()
defer registriesMutex.RUnlock()
models := make([]interface{}, 0, len(defaultRegistry.models))
for _, model := range defaultRegistry.models {
models = append(models, model)
var models []interface{}
seen := make(map[string]bool)
for _, registry := range registries {
registry.mutex.RLock()
for name, model := range registry.models {
// Only add the first occurrence of each model name
if !seen[name] {
models = append(models, model)
seen[name] = true
}
}
registry.mutex.RUnlock()
}
return models
}

View File

@@ -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

View File

@@ -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 {