mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-09 04:54:25 +00:00
* Implement ExtractTagValue to handle struct tag parsing. * Introduce GetRelationshipInfo for extracting relationship metadata. * Update tests to validate new functionality. * Refactor related code for improved clarity and maintainability.
264 lines
8.8 KiB
Go
264 lines
8.8 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
)
|
|
|
|
// ValidateAndUnwrapModelResult contains the result of model validation
|
|
type ValidateAndUnwrapModelResult struct {
|
|
ModelType reflect.Type
|
|
Model interface{}
|
|
ModelPtr interface{}
|
|
OriginalType reflect.Type
|
|
}
|
|
|
|
// ValidateAndUnwrapModel validates that a model is a struct type and unwraps
|
|
// pointers, slices, and arrays to get to the base struct type.
|
|
// Returns an error if the model is not a valid struct type.
|
|
func ValidateAndUnwrapModel(model interface{}) (*ValidateAndUnwrapModelResult, error) {
|
|
modelType := reflect.TypeOf(model)
|
|
originalType := modelType
|
|
|
|
// Unwrap pointers, slices, and arrays to get to the base struct type
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Validate that we have a struct type
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return nil, fmt.Errorf("model must be a struct type, got %v. Ensure you register the struct (e.g., ModelCoreAccount{}) not a slice (e.g., []*ModelCoreAccount)", originalType)
|
|
}
|
|
|
|
// If the registered model was a pointer or slice, use the unwrapped struct type
|
|
if originalType != modelType {
|
|
model = reflect.New(modelType).Elem().Interface()
|
|
}
|
|
|
|
// Create a pointer to the model type for database operations
|
|
modelPtr := reflect.New(reflect.TypeOf(model)).Interface()
|
|
|
|
return &ValidateAndUnwrapModelResult{
|
|
ModelType: modelType,
|
|
Model: model,
|
|
ModelPtr: modelPtr,
|
|
OriginalType: originalType,
|
|
}, nil
|
|
}
|
|
|
|
// ExtractTagValue extracts the value for a given key from a struct tag string.
|
|
// It handles both semicolon and comma-separated tag formats (e.g., GORM and BUN tags).
|
|
// For tags like "json:name;validate:required" it will extract "name" for key "json".
|
|
// For tags like "rel:has-many,join:table" it will extract "table" for key "join".
|
|
func ExtractTagValue(tag, key string) string {
|
|
// Split by both semicolons and commas to handle different tag formats
|
|
// We need to be smart about this - commas can be part of values
|
|
// So we'll try semicolon first, then comma if needed
|
|
separators := []string{";", ","}
|
|
|
|
for _, sep := range separators {
|
|
parts := strings.Split(tag, sep)
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if strings.HasPrefix(part, key+":") {
|
|
return strings.TrimPrefix(part, key+":")
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetRelationshipInfo analyzes a model type and extracts relationship metadata
|
|
// for a specific relation field identified by its JSON name.
|
|
// Returns nil if the field is not found or is not a valid relationship.
|
|
func GetRelationshipInfo(modelType reflect.Type, relationName string) *RelationshipInfo {
|
|
// Ensure we have a struct type
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
logger.Warn("Cannot get relationship info from non-struct type: %v", modelType)
|
|
return nil
|
|
}
|
|
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
field := modelType.Field(i)
|
|
jsonTag := field.Tag.Get("json")
|
|
jsonName := strings.Split(jsonTag, ",")[0]
|
|
|
|
if jsonName == relationName {
|
|
gormTag := field.Tag.Get("gorm")
|
|
bunTag := field.Tag.Get("bun")
|
|
info := &RelationshipInfo{
|
|
FieldName: field.Name,
|
|
JSONName: jsonName,
|
|
}
|
|
|
|
if strings.Contains(bunTag, "rel:") || strings.Contains(bunTag, "join:") {
|
|
//bun:"rel:has-many,join:rid_hub=rid_hub_division"
|
|
if strings.Contains(bunTag, "has-many") {
|
|
info.RelationType = "hasMany"
|
|
} else if strings.Contains(bunTag, "has-one") {
|
|
info.RelationType = "hasOne"
|
|
} else if strings.Contains(bunTag, "belongs-to") {
|
|
info.RelationType = "belongsTo"
|
|
} else if strings.Contains(bunTag, "many-to-many") {
|
|
info.RelationType = "many2many"
|
|
} else {
|
|
info.RelationType = "hasOne"
|
|
}
|
|
|
|
// Extract join info
|
|
joinPart := ExtractTagValue(bunTag, "join")
|
|
if joinPart != "" && info.RelationType == "many2many" {
|
|
// For many2many, the join part is the join table name
|
|
info.JoinTable = joinPart
|
|
} else if joinPart != "" {
|
|
// For other relations, parse foreignKey and references
|
|
joinParts := strings.Split(joinPart, "=")
|
|
if len(joinParts) == 2 {
|
|
info.ForeignKey = joinParts[0]
|
|
info.References = joinParts[1]
|
|
}
|
|
}
|
|
|
|
// Get related model type
|
|
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 if field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct {
|
|
elemType := field.Type
|
|
if elemType.Kind() == reflect.Ptr {
|
|
elemType = elemType.Elem()
|
|
}
|
|
if elemType.Kind() == reflect.Struct {
|
|
info.RelatedModel = reflect.New(elemType).Elem().Interface()
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// Parse GORM tag to determine relationship type and keys
|
|
if strings.Contains(gormTag, "foreignKey") {
|
|
info.ForeignKey = ExtractTagValue(gormTag, "foreignKey")
|
|
info.References = ExtractTagValue(gormTag, "references")
|
|
|
|
// 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 = 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 nil
|
|
}
|
|
|
|
// RelationPathToBunAlias converts a relation path (e.g., "Order.Customer") to a Bun alias format.
|
|
// It converts to lowercase and replaces dots with double underscores.
|
|
// For example: "Order.Customer" -> "order__customer"
|
|
func RelationPathToBunAlias(relationPath string) string {
|
|
if relationPath == "" {
|
|
return ""
|
|
}
|
|
// Convert to lowercase and replace dots with double underscores
|
|
alias := strings.ToLower(relationPath)
|
|
alias = strings.ReplaceAll(alias, ".", "__")
|
|
return alias
|
|
}
|
|
|
|
// ReplaceTableReferencesInSQL replaces references to a base table name in a SQL expression
|
|
// with the appropriate alias for the current preload level.
|
|
// For example, if baseTableName is "mastertaskitem" and targetAlias is "mal__mal",
|
|
// it will replace "mastertaskitem.rid_mastertaskitem" with "mal__mal.rid_mastertaskitem"
|
|
func ReplaceTableReferencesInSQL(sqlExpr, baseTableName, targetAlias string) string {
|
|
if sqlExpr == "" || baseTableName == "" || targetAlias == "" {
|
|
return sqlExpr
|
|
}
|
|
|
|
// Replace both quoted and unquoted table references
|
|
// Handle patterns like: tablename.column, "tablename".column, tablename."column", "tablename"."column"
|
|
|
|
// Pattern 1: tablename.column (unquoted)
|
|
result := strings.ReplaceAll(sqlExpr, baseTableName+".", targetAlias+".")
|
|
|
|
// Pattern 2: "tablename".column or "tablename"."column" (quoted table name)
|
|
result = strings.ReplaceAll(result, "\""+baseTableName+"\".", "\""+targetAlias+"\".")
|
|
|
|
return result
|
|
}
|
|
|
|
// GetTableNameFromModel extracts the table name from a model.
|
|
// It checks the bun tag first, then falls back to converting the struct name to snake_case.
|
|
func GetTableNameFromModel(model interface{}) string {
|
|
if model == nil {
|
|
return ""
|
|
}
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
|
|
// Unwrap pointers
|
|
for modelType != nil && modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return ""
|
|
}
|
|
|
|
// Look for bun tag on embedded BaseModel
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
field := modelType.Field(i)
|
|
if field.Anonymous {
|
|
bunTag := field.Tag.Get("bun")
|
|
if strings.HasPrefix(bunTag, "table:") {
|
|
return strings.TrimPrefix(bunTag, "table:")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: convert struct name to lowercase (simple heuristic)
|
|
// This handles cases like "MasterTaskItem" -> "mastertaskitem"
|
|
return strings.ToLower(modelType.Name())
|
|
}
|