Added recursive crud handler.

This commit is contained in:
Hein 2025-11-11 10:21:20 +02:00
parent 2818e7e9cd
commit dc3254522c
3 changed files with 1243 additions and 50 deletions

View File

@ -0,0 +1,417 @@
package common
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/logger"
)
// 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
}
// RelationshipInfo contains information about a model relationship
type RelationshipInfo struct {
FieldName string
JSONName string
RelationType string // "belongsTo", "hasMany", "hasOne", "many2many"
ForeignKey string
References string
JoinTable string
RelatedModel interface{}
}
// 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 crud_request field that overrides the operation
if requestOp := p.extractCRUDRequest(data); requestOp != "" {
logger.Debug("Found crud_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 {
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 crud_request field in actual data processing
if key == "crud_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
}
}
// Inject parent IDs for foreign key resolution
p.injectForeignKeys(regularData, modelType, parentIDs)
// Process based on operation
switch strings.ToLower(operation) {
case "insert", "create":
id, err := p.processInsert(ctx, regularData, tableName)
if err != nil {
return nil, fmt.Errorf("insert failed: %w", err)
}
result.ID = id
result.AffectedRows = 1
result.Data = regularData
// Process child relations after parent insert (to get parent ID)
if err := p.processChildRelations(ctx, "insert", id, relationFields, result.RelationData, modelType); err != nil {
return nil, fmt.Errorf("failed to process child relations: %w", err)
}
case "update":
rows, err := p.processUpdate(ctx, regularData, tableName, data["id"])
if err != nil {
return nil, fmt.Errorf("update failed: %w", err)
}
result.ID = data["id"]
result.AffectedRows = rows
result.Data = regularData
// Process child relations for update
if err := p.processChildRelations(ctx, "update", data["id"], relationFields, result.RelationData, modelType); err != nil {
return nil, fmt.Errorf("failed to process child relations: %w", err)
}
case "delete":
// Process child relations first (for referential integrity)
if err := p.processChildRelations(ctx, "delete", data["id"], relationFields, result.RelationData, modelType); err != nil {
return nil, fmt.Errorf("failed to process child relations before delete: %w", err)
}
rows, err := p.processDelete(ctx, tableName, data["id"])
if err != nil {
return nil, fmt.Errorf("delete failed: %w", err)
}
result.ID = data["id"]
result.AffectedRows = rows
result.Data = regularData
default:
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 crud_request field from data if present
func (p *NestedCUDProcessor) extractCRUDRequest(data map[string]interface{}) string {
if request, ok := data["crud_request"]; ok {
if requestStr, ok := request.(string); ok {
return strings.ToLower(strings.TrimSpace(requestStr))
}
}
return ""
}
// injectForeignKeys injects parent IDs into data for foreign key fields
func (p *NestedCUDProcessor) injectForeignKeys(data map[string]interface{}, modelType reflect.Type, parentIDs map[string]interface{}) {
if len(parentIDs) == 0 {
return
}
// Iterate through model fields to find foreign key fields
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
jsonTag := field.Tag.Get("json")
jsonName := strings.Split(jsonTag, ",")[0]
// Check if this field is a foreign key and we have a parent ID for it
// Common patterns: DepartmentID, ManagerID, ProjectID, etc.
for parentKey, parentID := range parentIDs {
// Match field name patterns like "department_id" with parent key "department"
if strings.EqualFold(jsonName, parentKey+"_id") ||
strings.EqualFold(jsonName, parentKey+"id") ||
strings.EqualFold(field.Name, parentKey+"ID") {
// Only inject if not already present
if _, exists := data[jsonName]; !exists {
logger.Debug("Injecting foreign key: %s = %v", jsonName, parentID)
data[jsonName] = 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, value)
}
// Add RETURNING clause to get the inserted ID
query = query.Returning("id")
result, err := query.Exec(ctx)
if err != nil {
return nil, fmt.Errorf("insert exec failed: %w", err)
}
// Try to get the ID
var id interface{}
if lastID, err := result.LastInsertId(); err == nil && lastID > 0 {
id = lastID
} else if data["id"] != nil {
id = data["id"]
}
logger.Debug("Insert successful, ID: %v, rows affected: %d", id, result.RowsAffected())
return id, 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 {
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("id = ?", id)
result, err := query.Exec(ctx)
if err != nil {
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 {
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("id = ?", id)
result, err := query.Exec(ctx)
if err != nil {
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,
) 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.Warn("Field %s not found in model", relInfo.FieldName)
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
parentIDs := make(map[string]interface{})
if relInfo.ForeignKey != "" {
// Extract the base name from foreign key (e.g., "DepartmentID" -> "Department")
baseName := strings.TrimSuffix(relInfo.ForeignKey, "ID")
baseName = strings.TrimSuffix(strings.ToLower(baseName), "_id")
parentIDs[baseName] = parentID
}
// Process based on relation type and data structure
switch v := relationValue.(type) {
case map[string]interface{}:
// Single related object
_, err := p.ProcessNestedCUD(ctx, operation, v, relatedModel, parentIDs, relatedTableName)
if err != nil {
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 {
_, err := p.ProcessNestedCUD(ctx, operation, itemMap, relatedModel, parentIDs, relatedTableName)
if err != nil {
return fmt.Errorf("failed to process relation %s[%d]: %w", relationName, i, err)
}
}
}
case []map[string]interface{}:
// Multiple related objects (typed slice)
for i, itemMap := range v {
_, err := p.ProcessNestedCUD(ctx, operation, itemMap, relatedModel, parentIDs, relatedTableName)
if err != nil {
return fmt.Errorf("failed to process relation %s[%d]: %w", relationName, i, err)
}
}
default:
logger.Warn("Unsupported relation data type for %s: %T", relationName, 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 checks if the data contains nested relations or a crud_request field
func ShouldUseNestedProcessor(data map[string]interface{}, model interface{}, relationshipHelper RelationshipInfoProvider) bool {
// Check for crud_request field
if _, hasCRUDRequest := data["crud_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 crud_request and regular scalar fields
if key == "crud_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 value.(type) {
case map[string]interface{}, []interface{}, []map[string]interface{}:
logger.Debug("Found nested relation field: %s", key)
return true
}
}
}
return false
}

View File

@ -15,16 +15,20 @@ import (
// Handler handles API requests using database and model abstractions // Handler handles API requests using database and model abstractions
type Handler struct { type Handler struct {
db common.Database db common.Database
registry common.ModelRegistry registry common.ModelRegistry
nestedProcessor *common.NestedCUDProcessor
} }
// NewHandler creates a new API handler with database and registry abstractions // NewHandler creates a new API handler with database and registry abstractions
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler { func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
return &Handler{ handler := &Handler{
db: db, db: db,
registry: registry, registry: registry,
} }
// Initialize nested processor
handler.nestedProcessor = common.NewNestedCUDProcessor(db, registry, handler)
return handler
} }
// handlePanic is a helper function to handle panics with stack traces // handlePanic is a helper function to handle panics with stack traces
@ -112,7 +116,7 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
case "update": case "update":
h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options) h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options)
case "delete": case "delete":
h.handleDelete(ctx, w, id) h.handleDelete(ctx, w, id, req.Data)
default: default:
logger.Error("Invalid operation: %s", req.Operation) logger.Error("Invalid operation: %s", req.Operation)
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil) h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
@ -286,13 +290,29 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
schema := GetSchema(ctx) schema := GetSchema(ctx)
entity := GetEntity(ctx) entity := GetEntity(ctx)
tableName := GetTableName(ctx) tableName := GetTableName(ctx)
model := GetModel(ctx)
logger.Info("Creating records for %s.%s", schema, entity) logger.Info("Creating records for %s.%s", schema, entity)
query := h.db.NewInsert().Table(tableName) // Check if data contains nested relations or crud_request field
switch v := data.(type) { switch v := data.(type) {
case map[string]interface{}: case map[string]interface{}:
// Check if we should use nested processing
if h.shouldUseNestedProcessor(v, model) {
logger.Info("Using nested CUD processor for create operation")
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", v, model, make(map[string]interface{}), tableName)
if err != nil {
logger.Error("Error in nested create: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record with nested data", err)
return
}
logger.Info("Successfully created record with nested data, ID: %v", result.ID)
h.sendResponse(w, result.Data, nil)
return
}
// Standard processing without nested relations
query := h.db.NewInsert().Table(tableName)
for key, value := range v { for key, value := range v {
query = query.Value(key, value) query = query.Value(key, value)
} }
@ -306,6 +326,46 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
h.sendResponse(w, v, nil) h.sendResponse(w, v, nil)
case []map[string]interface{}: case []map[string]interface{}:
// Check if any item needs nested processing
hasNestedData := false
for _, item := range v {
if h.shouldUseNestedProcessor(item, model) {
hasNestedData = true
break
}
}
if hasNestedData {
logger.Info("Using nested CUD processor for batch create with nested data")
results := make([]map[string]interface{}, 0, len(v))
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Temporarily swap the database to use transaction
originalDB := h.nestedProcessor
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
defer func() {
h.nestedProcessor = originalDB
}()
for _, item := range v {
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", item, model, make(map[string]interface{}), tableName)
if err != nil {
return fmt.Errorf("failed to process item: %w", err)
}
results = append(results, result.Data)
}
return nil
})
if err != nil {
logger.Error("Error creating records with nested data: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records with nested data", err)
return
}
logger.Info("Successfully created %d records with nested data", len(results))
h.sendResponse(w, results, nil)
return
}
// Standard batch insert without nested relations
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v { for _, item := range v {
txQuery := tx.NewInsert().Table(tableName) txQuery := tx.NewInsert().Table(tableName)
@ -328,6 +388,50 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
case []interface{}: case []interface{}:
// Handle []interface{} type from JSON unmarshaling // Handle []interface{} type from JSON unmarshaling
// Check if any item needs nested processing
hasNestedData := false
for _, item := range v {
if itemMap, ok := item.(map[string]interface{}); ok {
if h.shouldUseNestedProcessor(itemMap, model) {
hasNestedData = true
break
}
}
}
if hasNestedData {
logger.Info("Using nested CUD processor for batch create with nested data ([]interface{})")
results := make([]interface{}, 0, len(v))
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Temporarily swap the database to use transaction
originalDB := h.nestedProcessor
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
defer func() {
h.nestedProcessor = originalDB
}()
for _, item := range v {
if itemMap, ok := item.(map[string]interface{}); ok {
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", itemMap, model, make(map[string]interface{}), tableName)
if err != nil {
return fmt.Errorf("failed to process item: %w", err)
}
results = append(results, result.Data)
}
}
return nil
})
if err != nil {
logger.Error("Error creating records with nested data: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records with nested data", err)
return
}
logger.Info("Successfully created %d records with nested data", len(results))
h.sendResponse(w, results, nil)
return
}
// Standard batch insert without nested relations
list := make([]interface{}, 0) list := make([]interface{}, 0)
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v { for _, item := range v {
@ -369,53 +473,210 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
schema := GetSchema(ctx) schema := GetSchema(ctx)
entity := GetEntity(ctx) entity := GetEntity(ctx)
tableName := GetTableName(ctx) tableName := GetTableName(ctx)
model := GetModel(ctx)
logger.Info("Updating records for %s.%s", schema, entity) logger.Info("Updating records for %s.%s", schema, entity)
query := h.db.NewUpdate().Table(tableName)
switch updates := data.(type) { switch updates := data.(type) {
case map[string]interface{}: case map[string]interface{}:
query = query.SetMap(updates) // Determine the ID to use
var targetID interface{}
if urlID != "" {
targetID = urlID
} else if reqID != nil {
targetID = reqID
} else if updates["id"] != nil {
targetID = updates["id"]
}
// Check if we should use nested processing
if h.shouldUseNestedProcessor(updates, model) {
logger.Info("Using nested CUD processor for update operation")
// Ensure ID is in the data map
if targetID != nil {
updates["id"] = targetID
}
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "update", updates, model, make(map[string]interface{}), tableName)
if err != nil {
logger.Error("Error in nested update: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record with nested data", err)
return
}
logger.Info("Successfully updated record with nested data, rows: %d", result.AffectedRows)
h.sendResponse(w, result.Data, nil)
return
}
// Standard processing without nested relations
query := h.db.NewUpdate().Table(tableName).SetMap(updates)
// Apply conditions
if urlID != "" {
logger.Debug("Updating by URL ID: %s", urlID)
query = query.Where("id = ?", urlID)
} else if reqID != nil {
switch id := reqID.(type) {
case string:
logger.Debug("Updating by request ID: %s", id)
query = query.Where("id = ?", id)
case []string:
logger.Debug("Updating by multiple IDs: %v", id)
query = query.Where("id IN (?)", id)
}
}
result, err := query.Exec(ctx)
if err != nil {
logger.Error("Update error: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", err)
return
}
if result.RowsAffected() == 0 {
logger.Warn("No records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
return
}
logger.Info("Successfully updated %d records", result.RowsAffected())
h.sendResponse(w, data, nil)
case []map[string]interface{}:
// Batch update with array of objects
hasNestedData := false
for _, item := range updates {
if h.shouldUseNestedProcessor(item, model) {
hasNestedData = true
break
}
}
if hasNestedData {
logger.Info("Using nested CUD processor for batch update with nested data")
results := make([]map[string]interface{}, 0, len(updates))
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Temporarily swap the database to use transaction
originalDB := h.nestedProcessor
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
defer func() {
h.nestedProcessor = originalDB
}()
for _, item := range updates {
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "update", item, model, make(map[string]interface{}), tableName)
if err != nil {
return fmt.Errorf("failed to process item: %w", err)
}
results = append(results, result.Data)
}
return nil
})
if err != nil {
logger.Error("Error updating records with nested data: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records with nested data", err)
return
}
logger.Info("Successfully updated %d records with nested data", len(results))
h.sendResponse(w, results, nil)
return
}
// Standard batch update without nested relations
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range updates {
if itemID, ok := item["id"]; ok {
txQuery := tx.NewUpdate().Table(tableName).SetMap(item).Where("id = ?", itemID)
if _, err := txQuery.Exec(ctx); err != nil {
return err
}
}
}
return nil
})
if err != nil {
logger.Error("Error updating records: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err)
return
}
logger.Info("Successfully updated %d records", len(updates))
h.sendResponse(w, updates, nil)
case []interface{}:
// Batch update with []interface{}
hasNestedData := false
for _, item := range updates {
if itemMap, ok := item.(map[string]interface{}); ok {
if h.shouldUseNestedProcessor(itemMap, model) {
hasNestedData = true
break
}
}
}
if hasNestedData {
logger.Info("Using nested CUD processor for batch update with nested data ([]interface{})")
results := make([]interface{}, 0, len(updates))
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Temporarily swap the database to use transaction
originalDB := h.nestedProcessor
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
defer func() {
h.nestedProcessor = originalDB
}()
for _, item := range updates {
if itemMap, ok := item.(map[string]interface{}); ok {
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "update", itemMap, model, make(map[string]interface{}), tableName)
if err != nil {
return fmt.Errorf("failed to process item: %w", err)
}
results = append(results, result.Data)
}
}
return nil
})
if err != nil {
logger.Error("Error updating records with nested data: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records with nested data", err)
return
}
logger.Info("Successfully updated %d records with nested data", len(results))
h.sendResponse(w, results, nil)
return
}
// Standard batch update without nested relations
list := make([]interface{}, 0)
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range updates {
if itemMap, ok := item.(map[string]interface{}); ok {
if itemID, ok := itemMap["id"]; ok {
txQuery := tx.NewUpdate().Table(tableName).SetMap(itemMap).Where("id = ?", itemID)
if _, err := txQuery.Exec(ctx); err != nil {
return err
}
list = append(list, item)
}
}
}
return nil
})
if err != nil {
logger.Error("Error updating records: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err)
return
}
logger.Info("Successfully updated %d records", len(list))
h.sendResponse(w, list, nil)
default: default:
logger.Error("Invalid data type for update operation: %T", data) logger.Error("Invalid data type for update operation: %T", data)
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for update operation", nil) h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for update operation", nil)
return return
} }
// Apply conditions
if urlID != "" {
logger.Debug("Updating by URL ID: %s", urlID)
query = query.Where("id = ?", urlID)
} else if reqID != nil {
switch id := reqID.(type) {
case string:
logger.Debug("Updating by request ID: %s", id)
query = query.Where("id = ?", id)
case []string:
logger.Debug("Updating by multiple IDs: %v", id)
query = query.Where("id IN (?)", id)
}
}
result, err := query.Exec(ctx)
if err != nil {
logger.Error("Update error: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record(s)", err)
return
}
if result.RowsAffected() == 0 {
logger.Warn("No records found to update")
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
return
}
logger.Info("Successfully updated %d records", result.RowsAffected())
h.sendResponse(w, data, nil)
} }
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) { func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string, data interface{}) {
// Capture panics and return error response // Capture panics and return error response
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@ -429,6 +690,105 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
logger.Info("Deleting records from %s.%s", schema, entity) logger.Info("Deleting records from %s.%s", schema, entity)
// Handle batch delete from request data
if data != nil {
switch v := data.(type) {
case []string:
// Array of IDs as strings
logger.Info("Batch delete with %d IDs ([]string)", len(v))
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, itemID := range v {
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
if _, err := query.Exec(ctx); err != nil {
return fmt.Errorf("failed to delete record %s: %w", itemID, err)
}
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", len(v))
h.sendResponse(w, map[string]interface{}{"deleted": len(v)}, nil)
return
case []interface{}:
// Array of IDs or objects with ID field
logger.Info("Batch delete with %d items ([]interface{})", len(v))
deletedCount := 0
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
var itemID interface{}
// Check if item is a string ID or object with id field
if idStr, ok := item.(string); ok {
itemID = idStr
} else if itemMap, ok := item.(map[string]interface{}); ok {
itemID = itemMap["id"]
} else {
// Try to use the item directly as ID
itemID = item
}
if itemID == nil {
continue // Skip items without ID
}
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
result, err := query.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete record %v: %w", itemID, err)
}
deletedCount += int(result.RowsAffected())
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", deletedCount)
h.sendResponse(w, map[string]interface{}{"deleted": deletedCount}, nil)
return
case []map[string]interface{}:
// Array of objects with id field
logger.Info("Batch delete with %d items ([]map[string]interface{})", len(v))
deletedCount := 0
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
if itemID, ok := item["id"]; ok && itemID != nil {
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
result, err := query.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete record %v: %w", itemID, err)
}
deletedCount += int(result.RowsAffected())
}
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", deletedCount)
h.sendResponse(w, map[string]interface{}{"deleted": deletedCount}, nil)
return
case map[string]interface{}:
// Single object with id field
if itemID, ok := v["id"]; ok && itemID != nil {
id = fmt.Sprintf("%v", itemID)
}
}
}
// Single delete with URL ID
if id == "" { if id == "" {
logger.Error("Delete operation requires an ID") logger.Error("Delete operation requires an ID")
h.sendError(w, http.StatusBadRequest, "missing_id", "Delete operation requires an ID", nil) h.sendError(w, http.StatusBadRequest, "missing_id", "Delete operation requires an ID", nil)
@ -636,6 +996,12 @@ func (h *Handler) RegisterModel(schema, name string, model interface{}) error {
return h.registry.RegisterModel(fullname, model) return h.registry.RegisterModel(fullname, model)
} }
// shouldUseNestedProcessor determines if we should use nested CUD processing
// It checks if the data contains nested relations or a crud_request field
func (h *Handler) shouldUseNestedProcessor(data map[string]interface{}, model interface{}) bool {
return common.ShouldUseNestedProcessor(data, model, h)
}
// Helper functions // Helper functions
func getColumnType(field reflect.StructField) string { func getColumnType(field reflect.StructField) string {
@ -690,6 +1056,24 @@ func isNullable(field reflect.StructField) bool {
// Preload support functions // Preload support functions
// GetRelationshipInfo implements common.RelationshipInfoProvider interface
func (h *Handler) GetRelationshipInfo(modelType reflect.Type, relationName string) *common.RelationshipInfo {
info := h.getRelationshipInfo(modelType, relationName)
if info == nil {
return nil
}
// Convert internal type to common type
return &common.RelationshipInfo{
FieldName: info.fieldName,
JSONName: info.jsonName,
RelationType: info.relationType,
ForeignKey: info.foreignKey,
References: info.references,
JoinTable: info.joinTable,
RelatedModel: info.relatedModel,
}
}
type relationshipInfo struct { type relationshipInfo struct {
fieldName string fieldName string
jsonName string jsonName string

View File

@ -17,18 +17,22 @@ import (
// Handler handles API requests using database and model abstractions // Handler handles API requests using database and model abstractions
// This handler reads filters, columns, and options from HTTP headers // This handler reads filters, columns, and options from HTTP headers
type Handler struct { type Handler struct {
db common.Database db common.Database
registry common.ModelRegistry registry common.ModelRegistry
hooks *HookRegistry hooks *HookRegistry
nestedProcessor *common.NestedCUDProcessor
} }
// NewHandler creates a new API handler with database and registry abstractions // NewHandler creates a new API handler with database and registry abstractions
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler { func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
return &Handler{ handler := &Handler{
db: db, db: db,
registry: registry, registry: registry,
hooks: NewHookRegistry(), hooks: NewHookRegistry(),
} }
// Initialize nested processor
handler.nestedProcessor = common.NewNestedCUDProcessor(db, registry, handler)
return handler
} }
// Hooks returns the hook registry for this handler // Hooks returns the hook registry for this handler
@ -146,7 +150,16 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
} }
h.handleUpdate(ctx, w, id, nil, data, options) h.handleUpdate(ctx, w, id, nil, data, options)
case "DELETE": case "DELETE":
h.handleDelete(ctx, w, id) // Try to read body for batch delete support
var data interface{}
body, err := r.Body()
if err == nil && len(body) > 0 {
if err := json.Unmarshal(body, &data); err != nil {
logger.Warn("Failed to decode delete request body (will try single delete): %v", err)
data = nil
}
}
h.handleDelete(ctx, w, id, data)
default: default:
logger.Error("Invalid HTTP method: %s", method) logger.Error("Invalid HTTP method: %s", method)
h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil) h.sendError(w, http.StatusMethodNotAllowed, "invalid_method", "Invalid HTTP method", nil)
@ -456,6 +469,22 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
logger.Info("Creating record in %s.%s", schema, entity) logger.Info("Creating record in %s.%s", schema, entity)
// Check if data is a single map with nested relations
if dataMap, ok := data.(map[string]interface{}); ok {
if h.shouldUseNestedProcessor(dataMap, model) {
logger.Info("Using nested CUD processor for create operation")
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", dataMap, model, make(map[string]interface{}), tableName)
if err != nil {
logger.Error("Error in nested create: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record with nested data", err)
return
}
logger.Info("Successfully created record with nested data, ID: %v", result.ID)
h.sendResponse(w, result.Data, nil)
return
}
}
// Execute BeforeCreate hooks // Execute BeforeCreate hooks
hookCtx := &HookContext{ hookCtx := &HookContext{
Context: ctx, Context: ctx,
@ -483,6 +512,63 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array { if dataValue.Kind() == reflect.Slice || dataValue.Kind() == reflect.Array {
logger.Debug("Batch creation detected, count: %d", dataValue.Len()) logger.Debug("Batch creation detected, count: %d", dataValue.Len())
// Check if any item needs nested processing
hasNestedData := false
for i := 0; i < dataValue.Len(); i++ {
item := dataValue.Index(i).Interface()
if itemMap, ok := item.(map[string]interface{}); ok {
if h.shouldUseNestedProcessor(itemMap, model) {
hasNestedData = true
break
}
}
}
if hasNestedData {
logger.Info("Using nested CUD processor for batch create with nested data")
results := make([]interface{}, 0, dataValue.Len())
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// Temporarily swap the database to use transaction
originalDB := h.nestedProcessor
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
defer func() {
h.nestedProcessor = originalDB
}()
for i := 0; i < dataValue.Len(); i++ {
item := dataValue.Index(i).Interface()
if itemMap, ok := item.(map[string]interface{}); ok {
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", itemMap, model, make(map[string]interface{}), tableName)
if err != nil {
return fmt.Errorf("failed to process item: %w", err)
}
results = append(results, result.Data)
}
}
return nil
})
if err != nil {
logger.Error("Error creating records with nested data: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records with nested data", err)
return
}
// Execute AfterCreate hooks
hookCtx.Result = map[string]interface{}{"created": len(results), "data": results}
hookCtx.Error = nil
if err := h.hooks.Execute(AfterCreate, hookCtx); err != nil {
logger.Error("AfterCreate hook failed: %v", err)
h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
return
}
logger.Info("Successfully created %d records with nested data", len(results))
h.sendResponse(w, results, nil)
return
}
// Standard batch insert without nested relations
// Use transaction for batch insert // Use transaction for batch insert
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for i := 0; i < dataValue.Len(); i++ { for i := 0; i < dataValue.Len(); i++ {
@ -613,6 +699,46 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
logger.Info("Updating record in %s.%s", schema, entity) logger.Info("Updating record in %s.%s", schema, entity)
// Convert data to map first for nested processor check
dataMap, ok := data.(map[string]interface{})
if !ok {
jsonData, err := json.Marshal(data)
if err != nil {
logger.Error("Error marshaling data: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data format", err)
return
}
if err := json.Unmarshal(jsonData, &dataMap); err != nil {
logger.Error("Error unmarshaling data: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data format", err)
return
}
}
// Check if we should use nested processing
if h.shouldUseNestedProcessor(dataMap, model) {
logger.Info("Using nested CUD processor for update operation")
// Ensure ID is in the data map
var targetID interface{}
if id != "" {
targetID = id
} else if idPtr != nil {
targetID = *idPtr
}
if targetID != nil {
dataMap["id"] = targetID
}
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "update", dataMap, model, make(map[string]interface{}), tableName)
if err != nil {
logger.Error("Error in nested update: %v", err)
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating record with nested data", err)
return
}
logger.Info("Successfully updated record with nested data, rows: %d", result.AffectedRows)
h.sendResponse(w, result.Data, nil)
return
}
// Execute BeforeUpdate hooks // Execute BeforeUpdate hooks
hookCtx := &HookContext{ hookCtx := &HookContext{
Context: ctx, Context: ctx,
@ -636,8 +762,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// Use potentially modified data from hook context // Use potentially modified data from hook context
data = hookCtx.Data data = hookCtx.Data
// Convert data to map // Convert data to map (again if modified by hooks)
dataMap, ok := data.(map[string]interface{}) dataMap, ok = data.(map[string]interface{})
if !ok { if !ok {
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
@ -700,7 +826,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
h.sendResponse(w, responseData, nil) h.sendResponse(w, responseData, nil)
} }
func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string) { func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id string, data interface{}) {
// Capture panics and return error response // Capture panics and return error response
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@ -713,8 +839,186 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
tableName := GetTableName(ctx) tableName := GetTableName(ctx)
model := GetModel(ctx) model := GetModel(ctx)
logger.Info("Deleting record from %s.%s", schema, entity) logger.Info("Deleting record(s) from %s.%s", schema, entity)
// Handle batch delete from request data
if data != nil {
switch v := data.(type) {
case []string:
// Array of IDs as strings
logger.Info("Batch delete with %d IDs ([]string)", len(v))
deletedCount := 0
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, itemID := range v {
// Execute hooks for each item
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
TableName: tableName,
Model: model,
ID: itemID,
Writer: w,
}
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
logger.Warn("BeforeDelete hook failed for ID %s: %v", itemID, err)
continue
}
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
result, err := query.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete record %s: %w", itemID, err)
}
deletedCount += int(result.RowsAffected())
// Execute AfterDelete hook
hookCtx.Result = map[string]interface{}{"deleted": result.RowsAffected()}
hookCtx.Error = nil
if err := h.hooks.Execute(AfterDelete, hookCtx); err != nil {
logger.Warn("AfterDelete hook failed for ID %s: %v", itemID, err)
}
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", deletedCount)
h.sendResponse(w, map[string]interface{}{"deleted": deletedCount}, nil)
return
case []interface{}:
// Array of IDs or objects with ID field
logger.Info("Batch delete with %d items ([]interface{})", len(v))
deletedCount := 0
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
var itemID interface{}
// Check if item is a string ID or object with id field
if idStr, ok := item.(string); ok {
itemID = idStr
} else if itemMap, ok := item.(map[string]interface{}); ok {
itemID = itemMap["id"]
} else {
itemID = item
}
if itemID == nil {
continue
}
itemIDStr := fmt.Sprintf("%v", itemID)
// Execute hooks for each item
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
TableName: tableName,
Model: model,
ID: itemIDStr,
Writer: w,
}
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
logger.Warn("BeforeDelete hook failed for ID %v: %v", itemID, err)
continue
}
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
result, err := query.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete record %v: %w", itemID, err)
}
deletedCount += int(result.RowsAffected())
// Execute AfterDelete hook
hookCtx.Result = map[string]interface{}{"deleted": result.RowsAffected()}
hookCtx.Error = nil
if err := h.hooks.Execute(AfterDelete, hookCtx); err != nil {
logger.Warn("AfterDelete hook failed for ID %v: %v", itemID, err)
}
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", deletedCount)
h.sendResponse(w, map[string]interface{}{"deleted": deletedCount}, nil)
return
case []map[string]interface{}:
// Array of objects with id field
logger.Info("Batch delete with %d items ([]map[string]interface{})", len(v))
deletedCount := 0
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
for _, item := range v {
if itemID, ok := item["id"]; ok && itemID != nil {
itemIDStr := fmt.Sprintf("%v", itemID)
// Execute hooks for each item
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
TableName: tableName,
Model: model,
ID: itemIDStr,
Writer: w,
}
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
logger.Warn("BeforeDelete hook failed for ID %v: %v", itemID, err)
continue
}
query := tx.NewDelete().Table(tableName).Where("id = ?", itemID)
result, err := query.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete record %v: %w", itemID, err)
}
deletedCount += int(result.RowsAffected())
// Execute AfterDelete hook
hookCtx.Result = map[string]interface{}{"deleted": result.RowsAffected()}
hookCtx.Error = nil
if err := h.hooks.Execute(AfterDelete, hookCtx); err != nil {
logger.Warn("AfterDelete hook failed for ID %v: %v", itemID, err)
}
}
}
return nil
})
if err != nil {
logger.Error("Error in batch delete: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting records", err)
return
}
logger.Info("Successfully deleted %d records", deletedCount)
h.sendResponse(w, map[string]interface{}{"deleted": deletedCount}, nil)
return
case map[string]interface{}:
// Single object with id field
if itemID, ok := v["id"]; ok && itemID != nil {
id = fmt.Sprintf("%v", itemID)
}
}
}
// Single delete with URL ID
// Execute BeforeDelete hooks // Execute BeforeDelete hooks
hookCtx := &HookContext{ hookCtx := &HookContext{
Context: ctx, Context: ctx,
@ -1318,3 +1622,91 @@ func filterExtendedOptions(validator *common.ColumnValidator, options ExtendedRe
return filtered return filtered
} }
// shouldUseNestedProcessor determines if we should use nested CUD processing
// It checks if the data contains nested relations or a crud_request field
func (h *Handler) shouldUseNestedProcessor(data map[string]interface{}, model interface{}) bool {
return common.ShouldUseNestedProcessor(data, model, h)
}
// Relationship support functions for nested CUD processing
// GetRelationshipInfo implements common.RelationshipInfoProvider interface
func (h *Handler) GetRelationshipInfo(modelType reflect.Type, relationName string) *common.RelationshipInfo {
info := h.getRelationshipInfo(modelType, relationName)
if info == nil {
return nil
}
// Convert internal type to common type
return &common.RelationshipInfo{
FieldName: info.fieldName,
JSONName: info.jsonName,
RelationType: info.relationType,
ForeignKey: info.foreignKey,
References: info.references,
JoinTable: info.joinTable,
RelatedModel: info.relatedModel,
}
}
type relationshipInfo struct {
fieldName string
jsonName string
relationType string // "belongsTo", "hasMany", "hasOne", "many2many"
foreignKey string
references string
joinTable string
relatedModel interface{}
}
func (h *Handler) 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")
info := &relationshipInfo{
fieldName: field.Name,
jsonName: jsonName,
}
// Parse GORM tag to determine relationship type and keys
if strings.Contains(gormTag, "foreignKey") {
info.foreignKey = h.extractTagValue(gormTag, "foreignKey")
info.references = h.extractTagValue(gormTag, "references")
// Determine if it's belongsTo or hasMany/hasOne
if field.Type.Kind() == reflect.Slice {
info.relationType = "hasMany"
} else if field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct {
info.relationType = "belongsTo"
}
} else if strings.Contains(gormTag, "many2many") {
info.relationType = "many2many"
info.joinTable = h.extractTagValue(gormTag, "many2many")
}
return info
}
}
return nil
}
func (h *Handler) extractTagValue(tag, key string) string {
parts := strings.Split(tag, ";")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, key+":") {
return strings.TrimPrefix(part, key+":")
}
}
return ""
}