mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-11-13 09:53:53 +00:00
Added recursive crud handler.
This commit is contained in:
parent
2818e7e9cd
commit
dc3254522c
417
pkg/common/recursive_crud.go
Normal file
417
pkg/common/recursive_crud.go
Normal 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
|
||||
}
|
||||
@ -17,14 +17,18 @@ import (
|
||||
type Handler struct {
|
||||
db common.Database
|
||||
registry common.ModelRegistry
|
||||
nestedProcessor *common.NestedCUDProcessor
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
return &Handler{
|
||||
handler := &Handler{
|
||||
db: db,
|
||||
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
|
||||
@ -112,7 +116,7 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
case "update":
|
||||
h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options)
|
||||
case "delete":
|
||||
h.handleDelete(ctx, w, id)
|
||||
h.handleDelete(ctx, w, id, req.Data)
|
||||
default:
|
||||
logger.Error("Invalid operation: %s", req.Operation)
|
||||
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)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
model := GetModel(ctx)
|
||||
|
||||
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) {
|
||||
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 {
|
||||
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)
|
||||
|
||||
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 {
|
||||
for _, item := range v {
|
||||
txQuery := tx.NewInsert().Table(tableName)
|
||||
@ -328,6 +388,50 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
||||
|
||||
case []interface{}:
|
||||
// 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)
|
||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||
for _, item := range v {
|
||||
@ -369,19 +473,42 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
schema := GetSchema(ctx)
|
||||
entity := GetEntity(ctx)
|
||||
tableName := GetTableName(ctx)
|
||||
model := GetModel(ctx)
|
||||
|
||||
logger.Info("Updating records for %s.%s", schema, entity)
|
||||
|
||||
query := h.db.NewUpdate().Table(tableName)
|
||||
|
||||
switch updates := data.(type) {
|
||||
case map[string]interface{}:
|
||||
query = query.SetMap(updates)
|
||||
default:
|
||||
logger.Error("Invalid data type for update operation: %T", data)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for update operation", nil)
|
||||
// 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 != "" {
|
||||
@ -413,9 +540,143 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
||||
|
||||
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:
|
||||
logger.Error("Invalid data type for update operation: %T", data)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for update operation", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
defer func() {
|
||||
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)
|
||||
|
||||
// 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 == "" {
|
||||
logger.Error("Delete operation requires an ID")
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
func getColumnType(field reflect.StructField) string {
|
||||
@ -690,6 +1056,24 @@ func isNullable(field reflect.StructField) bool {
|
||||
|
||||
// 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 {
|
||||
fieldName string
|
||||
jsonName string
|
||||
|
||||
@ -20,15 +20,19 @@ type Handler struct {
|
||||
db common.Database
|
||||
registry common.ModelRegistry
|
||||
hooks *HookRegistry
|
||||
nestedProcessor *common.NestedCUDProcessor
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
return &Handler{
|
||||
handler := &Handler{
|
||||
db: db,
|
||||
registry: registry,
|
||||
hooks: NewHookRegistry(),
|
||||
}
|
||||
// Initialize nested processor
|
||||
handler.nestedProcessor = common.NewNestedCUDProcessor(db, registry, handler)
|
||||
return 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)
|
||||
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:
|
||||
logger.Error("Invalid HTTP method: %s", method)
|
||||
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)
|
||||
|
||||
// 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
|
||||
hookCtx := &HookContext{
|
||||
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 {
|
||||
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
|
||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||
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)
|
||||
|
||||
// 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
|
||||
hookCtx := &HookContext{
|
||||
Context: ctx,
|
||||
@ -636,8 +762,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
||||
// Use potentially modified data from hook context
|
||||
data = hookCtx.Data
|
||||
|
||||
// Convert data to map
|
||||
dataMap, ok := data.(map[string]interface{})
|
||||
// Convert data to map (again if modified by hooks)
|
||||
dataMap, ok = data.(map[string]interface{})
|
||||
if !ok {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@ -700,7 +826,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
||||
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
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@ -713,8 +839,186 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
||||
tableName := GetTableName(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
|
||||
hookCtx := &HookContext{
|
||||
Context: ctx,
|
||||
@ -1318,3 +1622,91 @@ func filterExtendedOptions(validator *common.ColumnValidator, options ExtendedRe
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user