mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-06-11 16:23:43 +00:00
b157379ff8
Frontend clients are sensitive to 204 No Content responses; always return 200 with an empty array/object and rely on X-No-Data-Found header to signal absence of records. Also treat "change" as an alias for "update" in recursive CUD processing.
633 lines
24 KiB
Go
633 lines
24 KiB
Go
package common
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
|
)
|
|
|
|
// CRUDRequestProvider interface for models that provide CRUD request strings
|
|
type CRUDRequestProvider interface {
|
|
GetRequest() string
|
|
}
|
|
|
|
// RelationshipInfoProvider interface for handlers that can provide relationship info
|
|
type RelationshipInfoProvider interface {
|
|
GetRelationshipInfo(modelType reflect.Type, relationName string) *RelationshipInfo
|
|
}
|
|
|
|
// NestedCUDProcessor handles recursive processing of nested object graphs
|
|
type NestedCUDProcessor struct {
|
|
db Database
|
|
registry ModelRegistry
|
|
relationshipHelper RelationshipInfoProvider
|
|
}
|
|
|
|
// NewNestedCUDProcessor creates a new nested CUD processor
|
|
func NewNestedCUDProcessor(db Database, registry ModelRegistry, relationshipHelper RelationshipInfoProvider) *NestedCUDProcessor {
|
|
return &NestedCUDProcessor{
|
|
db: db,
|
|
registry: registry,
|
|
relationshipHelper: relationshipHelper,
|
|
}
|
|
}
|
|
|
|
// ProcessResult contains the result of processing a CUD operation
|
|
type ProcessResult struct {
|
|
ID interface{} // The ID of the processed record
|
|
AffectedRows int64 // Number of rows affected
|
|
Data map[string]interface{} // The processed data
|
|
RelationData map[string]interface{} // Data from processed relations
|
|
}
|
|
|
|
// ProcessNestedCUD recursively processes nested object graphs for Create, Update, Delete operations
|
|
// with automatic foreign key resolution
|
|
func (p *NestedCUDProcessor) ProcessNestedCUD(
|
|
ctx context.Context,
|
|
operation string, // "insert", "update", or "delete"
|
|
data map[string]interface{},
|
|
model interface{},
|
|
parentIDs map[string]interface{}, // Parent IDs for foreign key resolution
|
|
tableName string,
|
|
) (*ProcessResult, error) {
|
|
logger.Info("Processing nested CUD: operation=%s, table=%s", operation, tableName)
|
|
|
|
result := &ProcessResult{
|
|
Data: make(map[string]interface{}),
|
|
RelationData: make(map[string]interface{}),
|
|
}
|
|
|
|
// Check if data has a _request field that overrides the operation
|
|
if requestOp := p.extractCRUDRequest(data); requestOp != "" {
|
|
logger.Debug("Found _request override: %s", requestOp)
|
|
operation = requestOp
|
|
}
|
|
|
|
// Get model type for reflection
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
logger.Error("Invalid model type: operation=%s, table=%s, modelType=%v, expected struct", operation, tableName, modelType)
|
|
return nil, fmt.Errorf("model must be a struct type, got %v", modelType)
|
|
}
|
|
|
|
// Separate relation fields from regular fields
|
|
relationFields := make(map[string]*RelationshipInfo)
|
|
regularData := make(map[string]interface{})
|
|
|
|
for key, value := range data {
|
|
// Skip _request field in actual data processing
|
|
if key == "_request" {
|
|
continue
|
|
}
|
|
|
|
// Check if this field is a relation
|
|
relInfo := p.relationshipHelper.GetRelationshipInfo(modelType, key)
|
|
if relInfo != nil {
|
|
relationFields[key] = relInfo
|
|
result.RelationData[key] = value
|
|
} else {
|
|
regularData[key] = value
|
|
}
|
|
}
|
|
|
|
// Filter regularData to only include fields that exist in the model,
|
|
// and translate JSON keys to their actual database column names.
|
|
regularData = p.filterValidFields(regularData, model)
|
|
|
|
// Inject parent IDs for foreign key resolution
|
|
p.injectForeignKeys(regularData, modelType, parentIDs)
|
|
|
|
// Get the primary key name for this model
|
|
pkName := reflection.GetPrimaryKeyName(model)
|
|
|
|
// Check if we have any data to process (besides _request)
|
|
hasData := len(regularData) > 0
|
|
|
|
// Process based on operation
|
|
switch strings.ToLower(operation) {
|
|
case "insert", "create":
|
|
// Only perform insert if we have data to insert
|
|
if hasData {
|
|
id, err := p.processInsert(ctx, regularData, tableName)
|
|
if err != nil {
|
|
logger.Error("Insert failed for table=%s, data=%+v, error=%v", tableName, regularData, err)
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
result.ID = id
|
|
result.AffectedRows = 1
|
|
result.Data = regularData
|
|
|
|
// Re-select the inserted row so result.Data reflects DB-generated defaults.
|
|
if row, err := p.processSelect(ctx, tableName, id); err != nil {
|
|
logger.Warn("Select after insert failed: table=%s, id=%v, error=%v", tableName, id, err)
|
|
} else if len(row) > 0 {
|
|
result.Data = row
|
|
}
|
|
|
|
// Process child relations after parent insert (to get parent ID)
|
|
if err := p.processChildRelations(ctx, "insert", id, relationFields, result.RelationData, modelType, parentIDs); err != nil {
|
|
logger.Error("Failed to process child relations after insert: table=%s, parentID=%v, relations=%+v, error=%v", tableName, id, relationFields, err)
|
|
return nil, fmt.Errorf("failed to process child relations: %w", err)
|
|
}
|
|
} else {
|
|
logger.Debug("Skipping insert for %s - no data columns besides _request", tableName)
|
|
}
|
|
|
|
case "update", "change":
|
|
// Only perform update if we have data to update
|
|
if hasData {
|
|
rows, err := p.processUpdate(ctx, regularData, tableName, data[pkName])
|
|
if err != nil {
|
|
logger.Error("Update failed for table=%s, id=%v, data=%+v, error=%v", tableName, data[pkName], regularData, err)
|
|
return nil, fmt.Errorf("update failed: %w", err)
|
|
}
|
|
result.ID = data[pkName]
|
|
result.AffectedRows = rows
|
|
result.Data = regularData
|
|
|
|
// Re-select the updated row so result.Data reflects current DB state.
|
|
if row, err := p.processSelect(ctx, tableName, result.ID); err != nil {
|
|
logger.Warn("Select after update failed: table=%s, id=%v, error=%v", tableName, result.ID, err)
|
|
} else if len(row) > 0 {
|
|
result.Data = row
|
|
}
|
|
|
|
// Process child relations for update
|
|
if err := p.processChildRelations(ctx, "update", data[pkName], relationFields, result.RelationData, modelType, parentIDs); err != nil {
|
|
logger.Error("Failed to process child relations after update: table=%s, parentID=%v, relations=%+v, error=%v", tableName, data[pkName], regularData, err)
|
|
return nil, fmt.Errorf("failed to process child relations: %w", err)
|
|
}
|
|
} else {
|
|
logger.Debug("Skipping update for %s - no data columns besides _request", tableName)
|
|
result.ID = data[pkName]
|
|
}
|
|
|
|
case "delete":
|
|
// Process child relations first (for referential integrity)
|
|
if err := p.processChildRelations(ctx, "delete", data[pkName], relationFields, result.RelationData, modelType, parentIDs); err != nil {
|
|
logger.Error("Failed to process child relations before delete: table=%s, id=%v, relations=%+v, error=%v", tableName, data[pkName], relationFields, err)
|
|
return nil, nil
|
|
}
|
|
|
|
rows, err := p.processDelete(ctx, tableName, data[pkName])
|
|
if err != nil {
|
|
logger.Error("Delete failed for table=%s, id=%v, error=%v", tableName, data[pkName], err)
|
|
return nil, fmt.Errorf("delete failed: %w", err)
|
|
}
|
|
result.ID = data[pkName]
|
|
result.AffectedRows = rows
|
|
result.Data = regularData
|
|
|
|
default:
|
|
logger.Error("Unsupported operation: %s for table=%s", operation, tableName)
|
|
return nil, fmt.Errorf("unsupported operation: %s", operation)
|
|
}
|
|
|
|
logger.Info("Nested CUD completed: operation=%s, id=%v, rows=%d", operation, result.ID, result.AffectedRows)
|
|
return result, nil
|
|
}
|
|
|
|
// extractCRUDRequest extracts the request field from data if present
|
|
func (p *NestedCUDProcessor) extractCRUDRequest(data map[string]interface{}) string {
|
|
if request, ok := data["_request"]; ok {
|
|
if requestStr, ok := request.(string); ok {
|
|
return strings.ToLower(strings.TrimSpace(requestStr))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// filterValidFields filters input data to only include fields that exist in the model,
|
|
// and translates JSON key names to their actual database column names.
|
|
// For example, a field tagged `json:"_changed_date" bun:"changed_date"` will be
|
|
// included in the result as "changed_date", not "_changed_date".
|
|
func (p *NestedCUDProcessor) filterValidFields(data map[string]interface{}, model interface{}) map[string]interface{} {
|
|
if len(data) == 0 {
|
|
return data
|
|
}
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return data
|
|
}
|
|
|
|
// Build a mapping from JSON key -> DB column name for all writable fields.
|
|
// This both validates which fields belong to the model and translates their names
|
|
// to the correct column names for use in SQL insert/update queries.
|
|
jsonToDBCol := reflection.BuildJSONToDBColumnMap(modelType)
|
|
|
|
filteredData := make(map[string]interface{})
|
|
for key, value := range data {
|
|
dbColName, exists := jsonToDBCol[key]
|
|
if exists {
|
|
filteredData[dbColName] = value
|
|
} else {
|
|
logger.Debug("Skipping invalid field '%s' - not found in model %v", key, modelType)
|
|
}
|
|
}
|
|
|
|
return filteredData
|
|
}
|
|
|
|
// injectForeignKeys injects parent IDs into data for foreign key fields.
|
|
// data is expected to be keyed by DB column names (as returned by filterValidFields).
|
|
func (p *NestedCUDProcessor) injectForeignKeys(data map[string]interface{}, modelType reflect.Type, parentIDs map[string]interface{}) {
|
|
if len(parentIDs) == 0 {
|
|
return
|
|
}
|
|
|
|
pkCol := reflection.GetPrimaryKeyName(reflect.New(modelType).Interface())
|
|
|
|
for parentKey, parentID := range parentIDs {
|
|
dbColNames := reflection.GetForeignKeyColumn(modelType, parentKey)
|
|
|
|
if len(dbColNames) == 0 {
|
|
// No explicit tag found — fall back to naming convention by scanning scalar fields.
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
field := modelType.Field(i)
|
|
jsonName := strings.Split(field.Tag.Get("json"), ",")[0]
|
|
if strings.EqualFold(jsonName, "rid"+parentKey) ||
|
|
strings.EqualFold(jsonName, "rid_"+parentKey) ||
|
|
strings.EqualFold(jsonName, "id_"+parentKey) ||
|
|
strings.EqualFold(jsonName, parentKey+"_id") ||
|
|
strings.EqualFold(jsonName, parentKey+"id") ||
|
|
strings.EqualFold(field.Name, parentKey+"ID") {
|
|
dbColNames = []string{reflection.GetColumnName(field)}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, dbColName := range dbColNames {
|
|
if pkCol != "" && strings.EqualFold(dbColName, pkCol) {
|
|
continue
|
|
}
|
|
if _, exists := data[dbColName]; !exists {
|
|
logger.Debug("Injecting foreign key: %s = %v", dbColName, parentID)
|
|
data[dbColName] = parentID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// processInsert handles insert operation
|
|
func (p *NestedCUDProcessor) processInsert(
|
|
ctx context.Context,
|
|
data map[string]interface{},
|
|
tableName string,
|
|
) (interface{}, error) {
|
|
logger.Debug("Inserting into %s with data: %+v", tableName, data)
|
|
|
|
query := p.db.NewInsert().Table(tableName)
|
|
|
|
for key, value := range data {
|
|
query = query.Value(key, ConvertSliceForBun(value))
|
|
}
|
|
pkName := reflection.GetPrimaryKeyName(tableName)
|
|
query = query.Returning(pkName)
|
|
|
|
var id interface{}
|
|
if err := query.Scan(ctx, &id); err != nil {
|
|
logger.Error("Insert execution failed: table=%s, data=%+v, error=%v", tableName, data, err)
|
|
return nil, fmt.Errorf("insert exec failed: %w", err)
|
|
}
|
|
|
|
logger.Debug("Insert successful, ID: %v", id)
|
|
return id, nil
|
|
}
|
|
|
|
// processSelect fetches the row identified by id from tableName into a flat map.
|
|
// Used to populate result.Data with the actual DB state after insert/update.
|
|
func (p *NestedCUDProcessor) processSelect(ctx context.Context, tableName string, id interface{}) (map[string]interface{}, error) {
|
|
pkName := reflection.GetPrimaryKeyName(tableName)
|
|
var row map[string]interface{}
|
|
if err := p.db.NewSelect().
|
|
Table(tableName).
|
|
Where(fmt.Sprintf("%s = ?", QuoteIdent(pkName)), id).
|
|
Scan(ctx, &row); err != nil {
|
|
return nil, fmt.Errorf("select after write failed: %w", err)
|
|
}
|
|
return row, nil
|
|
}
|
|
|
|
// processUpdate handles update operation
|
|
func (p *NestedCUDProcessor) processUpdate(
|
|
ctx context.Context,
|
|
data map[string]interface{},
|
|
tableName string,
|
|
id interface{},
|
|
) (int64, error) {
|
|
if id == nil {
|
|
logger.Error("Update requires an ID: table=%s, data=%+v", tableName, data)
|
|
return 0, fmt.Errorf("update requires an ID")
|
|
}
|
|
|
|
logger.Debug("Updating %s with ID %v, data: %+v", tableName, id, data)
|
|
|
|
query := p.db.NewUpdate().Table(tableName).SetMap(data).Where(fmt.Sprintf("%s = ?", QuoteIdent(reflection.GetPrimaryKeyName(tableName))), id)
|
|
|
|
result, err := query.Exec(ctx)
|
|
if err != nil {
|
|
logger.Error("Update execution failed: table=%s, id=%v, data=%+v, error=%v", tableName, id, data, err)
|
|
return 0, fmt.Errorf("update exec failed: %w", err)
|
|
}
|
|
|
|
rows := result.RowsAffected()
|
|
logger.Debug("Update successful, rows affected: %d", rows)
|
|
return rows, nil
|
|
}
|
|
|
|
// processDelete handles delete operation
|
|
func (p *NestedCUDProcessor) processDelete(ctx context.Context, tableName string, id interface{}) (int64, error) {
|
|
if id == nil {
|
|
logger.Error("Delete requires an ID: table=%s", tableName)
|
|
return 0, fmt.Errorf("delete requires an ID")
|
|
}
|
|
|
|
logger.Debug("Deleting from %s with ID %v", tableName, id)
|
|
|
|
query := p.db.NewDelete().Table(tableName).Where(fmt.Sprintf("%s = ?", QuoteIdent(reflection.GetPrimaryKeyName(tableName))), id)
|
|
|
|
result, err := query.Exec(ctx)
|
|
if err != nil {
|
|
logger.Error("Delete execution failed: table=%s, id=%v, error=%v", tableName, id, err)
|
|
return 0, fmt.Errorf("delete exec failed: %w", err)
|
|
}
|
|
|
|
rows := result.RowsAffected()
|
|
logger.Debug("Delete successful, rows affected: %d", rows)
|
|
return rows, nil
|
|
}
|
|
|
|
// processChildRelations recursively processes child relations
|
|
func (p *NestedCUDProcessor) processChildRelations(
|
|
ctx context.Context,
|
|
operation string,
|
|
parentID interface{},
|
|
relationFields map[string]*RelationshipInfo,
|
|
relationData map[string]interface{},
|
|
parentModelType reflect.Type,
|
|
incomingParentIDs map[string]interface{}, // IDs from all ancestors
|
|
) error {
|
|
for relationName, relInfo := range relationFields {
|
|
relationValue, exists := relationData[relationName]
|
|
if !exists || relationValue == nil {
|
|
continue
|
|
}
|
|
|
|
logger.Debug("Processing relation: %s, type: %s", relationName, relInfo.RelationType)
|
|
|
|
// Get the related model
|
|
field, found := parentModelType.FieldByName(relInfo.FieldName)
|
|
if !found {
|
|
logger.Error("Field %s not found in model type %v for relation %s", relInfo.FieldName, parentModelType, relationName)
|
|
continue
|
|
}
|
|
|
|
// Get the model type for the relation
|
|
relatedModelType := field.Type
|
|
if relatedModelType.Kind() == reflect.Slice {
|
|
relatedModelType = relatedModelType.Elem()
|
|
}
|
|
if relatedModelType.Kind() == reflect.Ptr {
|
|
relatedModelType = relatedModelType.Elem()
|
|
}
|
|
|
|
// Create an instance of the related model
|
|
relatedModel := reflect.New(relatedModelType).Elem().Interface()
|
|
|
|
// Get table name for related model
|
|
relatedTableName := p.getTableNameForModel(relatedModel, relInfo.JSONName)
|
|
|
|
// Prepare parent IDs for foreign key injection
|
|
// Start by copying all incoming parent IDs (from ancestors)
|
|
parentIDs := make(map[string]interface{})
|
|
for k, v := range incomingParentIDs {
|
|
parentIDs[k] = v
|
|
}
|
|
logger.Debug("Inherited %d parent IDs from ancestors: %+v", len(incomingParentIDs), incomingParentIDs)
|
|
|
|
// Add the current parent's primary key to the parentIDs map
|
|
// This ensures nested children have access to all ancestor IDs
|
|
if parentID != nil && parentModelType != nil {
|
|
// Get the parent model's primary key field name
|
|
parentPKFieldName := reflection.GetPrimaryKeyName(parentModelType)
|
|
if parentPKFieldName != "" {
|
|
// Get the JSON name for the primary key field
|
|
parentPKJSONName := reflection.GetJSONNameForField(parentModelType, parentPKFieldName)
|
|
baseName := ""
|
|
if len(parentPKJSONName) > 1 {
|
|
baseName = parentPKJSONName
|
|
} else {
|
|
// Add parent's PK to the map using the base model name
|
|
baseName = strings.TrimSuffix(parentPKFieldName, "ID")
|
|
baseName = strings.TrimSuffix(strings.ToLower(baseName), "_id")
|
|
if baseName == "" {
|
|
baseName = "parent"
|
|
}
|
|
}
|
|
|
|
parentIDs[baseName] = parentID
|
|
logger.Debug("Added current parent PK to parentIDs map: %s=%v (from field %s)", baseName, parentID, parentPKFieldName)
|
|
}
|
|
}
|
|
|
|
// Also add the foreign key reference if specified
|
|
if relInfo.ForeignKey != "" && parentID != nil {
|
|
// Extract the base name from foreign key (e.g., "DepartmentID" -> "Department")
|
|
baseName := strings.TrimSuffix(relInfo.ForeignKey, "ID")
|
|
baseName = strings.TrimSuffix(strings.ToLower(baseName), "_id")
|
|
// Only add if different from what we already added
|
|
if _, exists := parentIDs[baseName]; !exists {
|
|
parentIDs[baseName] = parentID
|
|
logger.Debug("Added foreign key to parentIDs map: %s=%v (from FK %s)", baseName, parentID, relInfo.ForeignKey)
|
|
}
|
|
}
|
|
|
|
logger.Debug("Final parentIDs map for relation %s: %+v", relationName, parentIDs)
|
|
|
|
// Determine which field name to use for setting parent ID in child data
|
|
// Priority: Use foreign key field name if specified
|
|
var foreignKeyFieldName string
|
|
if relInfo.ForeignKey != "" {
|
|
// Get the JSON name for the foreign key field in the child model
|
|
foreignKeyFieldName = reflection.GetJSONNameForField(relatedModelType, relInfo.ForeignKey)
|
|
if foreignKeyFieldName == "" {
|
|
// Fallback to lowercase field name
|
|
foreignKeyFieldName = strings.ToLower(relInfo.ForeignKey)
|
|
}
|
|
logger.Debug("Using foreign key field for direct assignment: %s (from FK %s)", foreignKeyFieldName, relInfo.ForeignKey)
|
|
}
|
|
|
|
// Get the primary key name for the child model to avoid overwriting it in recursive relationships
|
|
childPKName := reflection.GetPrimaryKeyName(relatedModel)
|
|
childPKFieldName := reflection.GetJSONNameForField(relatedModelType, childPKName)
|
|
if childPKFieldName == "" {
|
|
childPKFieldName = strings.ToLower(childPKName)
|
|
}
|
|
|
|
logger.Debug("Processing relation with foreignKeyField=%s, childPK=%s", foreignKeyFieldName, childPKFieldName)
|
|
|
|
// Process based on relation type and data structure
|
|
switch v := relationValue.(type) {
|
|
case map[string]interface{}:
|
|
// Single related object - directly set foreign key if specified
|
|
// IMPORTANT: In recursive relationships, don't overwrite the primary key
|
|
if parentID != nil && foreignKeyFieldName != "" && foreignKeyFieldName != childPKFieldName {
|
|
v[foreignKeyFieldName] = parentID
|
|
logger.Debug("Set foreign key in single relation: %s=%v", foreignKeyFieldName, parentID)
|
|
} else if foreignKeyFieldName == childPKFieldName {
|
|
logger.Debug("Skipping foreign key assignment - same as primary key (recursive relationship): %s", foreignKeyFieldName)
|
|
}
|
|
_, err := p.ProcessNestedCUD(ctx, operation, v, relatedModel, parentIDs, relatedTableName)
|
|
if err != nil {
|
|
logger.Error("Failed to process single relation: name=%s, table=%s, operation=%s, parentID=%v, data=%+v, error=%v",
|
|
relationName, relatedTableName, operation, parentID, v, err)
|
|
return fmt.Errorf("failed to process relation %s: %w", relationName, err)
|
|
}
|
|
|
|
case []interface{}:
|
|
// Multiple related objects
|
|
for i, item := range v {
|
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
|
// Directly set foreign key if specified
|
|
// IMPORTANT: In recursive relationships, don't overwrite the primary key
|
|
if parentID != nil && foreignKeyFieldName != "" && foreignKeyFieldName != childPKFieldName {
|
|
itemMap[foreignKeyFieldName] = parentID
|
|
logger.Debug("Set foreign key in relation array[%d]: %s=%v", i, foreignKeyFieldName, parentID)
|
|
} else if foreignKeyFieldName == childPKFieldName {
|
|
logger.Debug("Skipping foreign key assignment in array[%d] - same as primary key (recursive relationship): %s", i, foreignKeyFieldName)
|
|
}
|
|
_, err := p.ProcessNestedCUD(ctx, operation, itemMap, relatedModel, parentIDs, relatedTableName)
|
|
if err != nil {
|
|
logger.Error("Failed to process relation array item: name=%s[%d], table=%s, operation=%s, parentID=%v, data=%+v, error=%v",
|
|
relationName, i, relatedTableName, operation, parentID, itemMap, err)
|
|
return fmt.Errorf("failed to process relation %s[%d]: %w", relationName, i, err)
|
|
}
|
|
} else {
|
|
logger.Warn("Relation array item is not a map: name=%s[%d], type=%T", relationName, i, item)
|
|
}
|
|
}
|
|
|
|
case []map[string]interface{}:
|
|
// Multiple related objects (typed slice)
|
|
for i, itemMap := range v {
|
|
// Directly set foreign key if specified
|
|
// IMPORTANT: In recursive relationships, don't overwrite the primary key
|
|
if parentID != nil && foreignKeyFieldName != "" && foreignKeyFieldName != childPKFieldName {
|
|
itemMap[foreignKeyFieldName] = parentID
|
|
logger.Debug("Set foreign key in relation typed array[%d]: %s=%v", i, foreignKeyFieldName, parentID)
|
|
} else if foreignKeyFieldName == childPKFieldName {
|
|
logger.Debug("Skipping foreign key assignment in typed array[%d] - same as primary key (recursive relationship): %s", i, foreignKeyFieldName)
|
|
}
|
|
_, err := p.ProcessNestedCUD(ctx, operation, itemMap, relatedModel, parentIDs, relatedTableName)
|
|
if err != nil {
|
|
logger.Error("Failed to process relation typed array item: name=%s[%d], table=%s, operation=%s, parentID=%v, data=%+v, error=%v",
|
|
relationName, i, relatedTableName, operation, parentID, itemMap, err)
|
|
return fmt.Errorf("failed to process relation %s[%d]: %w", relationName, i, err)
|
|
}
|
|
}
|
|
|
|
default:
|
|
logger.Error("Unsupported relation data type: name=%s, type=%T, value=%+v", relationName, relationValue, relationValue)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTableNameForModel gets the table name for a model
|
|
func (p *NestedCUDProcessor) getTableNameForModel(model interface{}, defaultName string) string {
|
|
if provider, ok := model.(TableNameProvider); ok {
|
|
tableName := provider.TableName()
|
|
if tableName != "" {
|
|
return tableName
|
|
}
|
|
}
|
|
return defaultName
|
|
}
|
|
|
|
// ShouldUseNestedProcessor determines if we should use nested CUD processing
|
|
// 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
|
|
}
|
|
|
|
// Get model type
|
|
modelType := reflect.TypeOf(model)
|
|
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
|
|
// Check if data contains any fields that are relations (nested objects or arrays)
|
|
for key, value := range data {
|
|
// Skip _request and regular scalar fields
|
|
if key == "_request" {
|
|
continue
|
|
}
|
|
|
|
// Check if this field is a relation in the model
|
|
relInfo := relationshipHelper.GetRelationshipInfo(modelType, key)
|
|
if relInfo != nil {
|
|
// Check if the value is actually nested data (object or array)
|
|
switch v := value.(type) {
|
|
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
|
|
}
|