ResolveSpec/pkg/resolvespec/handler.go
2025-11-11 15:34:24 +02:00

1243 lines
40 KiB
Go

package resolvespec
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"runtime/debug"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/logger"
"github.com/bitechdev/ResolveSpec/pkg/reflection"
)
// Handler handles API requests using database and model abstractions
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 {
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
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
stack := debug.Stack()
logger.Error("Panic in %s: %v\nStack trace:\n%s", method, err, string(stack))
h.sendError(w, http.StatusInternalServerError, "internal_error", fmt.Sprintf("Internal server error in %s", method), fmt.Errorf("%v", err))
}
// Handle processes API requests through router-agnostic interface
func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[string]string) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "Handle", err)
}
}()
ctx := context.Background()
body, err := r.Body()
if err != nil {
logger.Error("Failed to read request body: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_request", "Failed to read request body", err)
return
}
var req common.RequestBody
if err := json.Unmarshal(body, &req); err != nil {
logger.Error("Failed to decode request body: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_request", "Invalid request body", err)
return
}
schema := params["schema"]
entity := params["entity"]
id := params["id"]
logger.Info("Handling %s operation for %s.%s", req.Operation, schema, entity)
// Get model and populate context with request-scoped data
model, err := h.registry.GetModelByEntity(schema, entity)
if err != nil {
logger.Error("Invalid entity: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
return
}
// Validate that the model is a struct type (not a slice or pointer to slice)
modelType := reflect.TypeOf(model)
originalType := modelType
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
modelType = modelType.Elem()
}
if modelType == nil || modelType.Kind() != reflect.Struct {
logger.Error("Model for %s.%s must be a struct type, got %v. Please register models as struct types, not slices or pointers to slices.", schema, entity, originalType)
h.sendError(w, http.StatusInternalServerError, "invalid_model_type",
fmt.Sprintf("Model must be a struct type, got %v. Ensure you register the struct (e.g., ModelCoreAccount{}) not a slice (e.g., []*ModelCoreAccount)", originalType),
fmt.Errorf("invalid model type: %v", originalType))
return
}
// If the registered model was a pointer or slice, use the unwrapped struct type
if originalType != modelType {
model = reflect.New(modelType).Elem().Interface()
}
// Create a pointer to the model type for database operations
modelPtr := reflect.New(reflect.TypeOf(model)).Interface()
tableName := h.getTableName(schema, entity, model)
// Add request-scoped data to context
ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr)
// Validate and filter columns in options (log warnings for invalid columns)
validator := common.NewColumnValidator(model)
req.Options = validator.FilterRequestOptions(req.Options)
switch req.Operation {
case "read":
h.handleRead(ctx, w, id, req.Options)
case "create":
h.handleCreate(ctx, w, req.Data, req.Options)
case "update":
h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options)
case "delete":
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)
}
}
// HandleGet processes GET requests for metadata
func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params map[string]string) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "HandleGet", err)
}
}()
schema := params["schema"]
entity := params["entity"]
logger.Info("Getting metadata for %s.%s", schema, entity)
model, err := h.registry.GetModelByEntity(schema, entity)
if err != nil {
logger.Error("Failed to get model: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
return
}
metadata := h.generateMetadata(schema, entity, model)
h.sendResponse(w, metadata, nil)
}
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options common.RequestOptions) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleRead", err)
}
}()
schema := GetSchema(ctx)
entity := GetEntity(ctx)
tableName := GetTableName(ctx)
model := GetModel(ctx)
// Validate and unwrap model type to get base struct
modelType := reflect.TypeOf(model)
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
modelType = modelType.Elem()
}
if modelType == nil || modelType.Kind() != reflect.Struct {
logger.Error("Model must be a struct type, got %v for %s.%s", modelType, schema, entity)
h.sendError(w, http.StatusInternalServerError, "invalid_model", "Model must be a struct type", fmt.Errorf("invalid model type: %v", modelType))
return
}
logger.Info("Reading records from %s.%s", schema, entity)
// Create the model pointer for Scan() operations
sliceType := reflect.SliceOf(reflect.PointerTo(modelType))
modelPtr := reflect.New(sliceType).Interface()
// Start with Model() using the slice pointer to avoid "Model(nil)" errors in Count()
// Bun's Model() accepts both single pointers and slice pointers
query := h.db.NewSelect().Model(modelPtr)
// Only set Table() if the model doesn't provide a table name via the underlying type
// Create a temporary instance to check for TableNameProvider
tempInstance := reflect.New(modelType).Interface()
if provider, ok := tempInstance.(common.TableNameProvider); !ok || provider.TableName() == "" {
query = query.Table(tableName)
}
// Apply column selection
if len(options.Columns) > 0 {
logger.Debug("Selecting columns: %v", options.Columns)
query = query.Column(options.Columns...)
}
if len(options.ComputedColumns) > 0 {
for _, cu := range options.ComputedColumns {
logger.Debug("Applying computed column: %s", cu.Name)
query = query.ColumnExpr("(?) AS "+cu.Name, cu.Expression)
}
}
// Apply preloading
if len(options.Preload) > 0 {
query = h.applyPreloads(model, query, options.Preload)
}
// Apply filters
for _, filter := range options.Filters {
logger.Debug("Applying filter: %s %s %v", filter.Column, filter.Operator, filter.Value)
query = h.applyFilter(query, filter)
}
// Apply sorting
for _, sort := range options.Sort {
direction := "ASC"
if strings.EqualFold(sort.Direction, "desc") {
direction = "DESC"
}
logger.Debug("Applying sort: %s %s", sort.Column, direction)
query = query.Order(fmt.Sprintf("%s %s", sort.Column, direction))
}
// Get total count before pagination
total, err := query.Count(ctx)
if err != nil {
logger.Error("Error counting records: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error counting records", err)
return
}
logger.Debug("Total records before filtering: %d", total)
// Apply pagination
if options.Limit != nil && *options.Limit > 0 {
logger.Debug("Applying limit: %d", *options.Limit)
query = query.Limit(*options.Limit)
}
if options.Offset != nil && *options.Offset > 0 {
logger.Debug("Applying offset: %d", *options.Offset)
query = query.Offset(*options.Offset)
}
// Execute query
var result interface{}
if id != "" {
logger.Debug("Querying single record with ID: %s", id)
// For single record, create a new pointer to the struct type
singleResult := reflect.New(modelType).Interface()
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(singleResult))), id)
if err := query.Scan(ctx, singleResult); err != nil {
logger.Error("Error querying record: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
return
}
result = singleResult
} else {
logger.Debug("Querying multiple records")
// Use the modelPtr already created and set on the query
if err := query.Scan(ctx, modelPtr); err != nil {
logger.Error("Error querying records: %v", err)
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
return
}
result = reflect.ValueOf(modelPtr).Elem().Interface()
}
logger.Info("Successfully retrieved records")
limit := 0
if options.Limit != nil {
limit = *options.Limit
}
offset := 0
if options.Offset != nil {
offset = *options.Offset
}
h.sendResponse(w, result, &common.Metadata{
Total: int64(total),
Filtered: int64(total),
Limit: limit,
Offset: offset,
})
}
func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, data interface{}, options common.RequestOptions) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleCreate", err)
}
}()
schema := GetSchema(ctx)
entity := GetEntity(ctx)
tableName := GetTableName(ctx)
model := GetModel(ctx)
logger.Info("Creating records for %s.%s", schema, entity)
// Check if data contains nested relations or _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)
}
result, err := query.Exec(ctx)
if err != nil {
logger.Error("Error creating record: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", err)
return
}
logger.Info("Successfully created record, rows affected: %d", result.RowsAffected())
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)
for key, value := range item {
txQuery = txQuery.Value(key, value)
}
if _, err := txQuery.Exec(ctx); err != nil {
return err
}
}
return nil
})
if err != nil {
logger.Error("Error creating records: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", err)
return
}
logger.Info("Successfully created %d records", len(v))
h.sendResponse(w, v, nil)
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 {
if itemMap, ok := item.(map[string]interface{}); ok {
txQuery := tx.NewInsert().Table(tableName)
for key, value := range itemMap {
txQuery = txQuery.Value(key, value)
}
if _, err := txQuery.Exec(ctx); err != nil {
return err
}
list = append(list, item)
}
}
return nil
})
if err != nil {
logger.Error("Error creating records: %v", err)
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating records", err)
return
}
logger.Info("Successfully created %d records", len(v))
h.sendResponse(w, list, nil)
default:
logger.Error("Invalid data type for create operation: %T", data)
h.sendError(w, http.StatusBadRequest, "invalid_data", "Invalid data type for create operation", nil)
}
}
func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, urlID string, reqID interface{}, data interface{}, options common.RequestOptions) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleUpdate", err)
}
}()
schema := GetSchema(ctx)
entity := GetEntity(ctx)
tableName := GetTableName(ctx)
model := GetModel(ctx)
logger.Info("Updating records for %s.%s", schema, entity)
switch updates := data.(type) {
case map[string]interface{}:
// Determine the ID to use
var targetID interface{}
switch {
case urlID != "":
targetID = urlID
case reqID != nil:
targetID = reqID
case 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(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), urlID)
} else if reqID != nil {
switch id := reqID.(type) {
case string:
logger.Debug("Updating by request ID: %s", id)
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), id)
case []string:
logger.Debug("Updating by multiple IDs: %v", id)
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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, data interface{}) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleDelete", err)
}
}()
schema := GetSchema(ctx)
entity := GetEntity(ctx)
tableName := GetTableName(ctx)
model := GetModel(ctx)
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(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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
switch v := item.(type) {
case string:
itemID = v
case map[string]interface{}:
itemID = v["id"]
default:
// Try to use the item directly as ID
itemID = item
}
if itemID == nil {
continue // Skip items without ID
}
query := tx.NewDelete().Table(tableName).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), 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)
return
}
query := h.db.NewDelete().Table(tableName).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), id)
result, err := query.Exec(ctx)
if err != nil {
logger.Error("Error deleting record: %v", err)
h.sendError(w, http.StatusInternalServerError, "delete_error", "Error deleting record", err)
return
}
if result.RowsAffected() == 0 {
logger.Warn("No record found to delete with ID: %s", id)
h.sendError(w, http.StatusNotFound, "not_found", "Record not found", nil)
return
}
logger.Info("Successfully deleted record with ID: %s", id)
h.sendResponse(w, nil, nil)
}
func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOption) common.SelectQuery {
switch filter.Operator {
case "eq":
return query.Where(fmt.Sprintf("%s = ?", filter.Column), filter.Value)
case "neq":
return query.Where(fmt.Sprintf("%s != ?", filter.Column), filter.Value)
case "gt":
return query.Where(fmt.Sprintf("%s > ?", filter.Column), filter.Value)
case "gte":
return query.Where(fmt.Sprintf("%s >= ?", filter.Column), filter.Value)
case "lt":
return query.Where(fmt.Sprintf("%s < ?", filter.Column), filter.Value)
case "lte":
return query.Where(fmt.Sprintf("%s <= ?", filter.Column), filter.Value)
case "like":
return query.Where(fmt.Sprintf("%s LIKE ?", filter.Column), filter.Value)
case "ilike":
return query.Where(fmt.Sprintf("%s ILIKE ?", filter.Column), filter.Value)
case "in":
return query.Where(fmt.Sprintf("%s IN (?)", filter.Column), filter.Value)
default:
return query
}
}
// parseTableName splits a table name that may contain schema into separate schema and table
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
return fullTableName[:idx], fullTableName[idx+1:]
}
return "", fullTableName
}
// getSchemaAndTable returns the schema and table name separately
// It checks SchemaProvider and TableNameProvider interfaces and handles cases where
// the table name may already include the schema (e.g., "public.users")
//
// Priority order:
// 1. If TableName() contains a schema (e.g., "myschema.mytable"), that schema takes precedence
// 2. If model implements SchemaProvider, use that schema
// 3. Otherwise, use the defaultSchema parameter
func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interface{}) (schema, table string) {
// First check if model provides a table name
// We check this FIRST because the table name might already contain the schema
if tableProvider, ok := model.(common.TableNameProvider); ok {
tableName := tableProvider.TableName()
// IMPORTANT: Check if the table name already contains a schema (e.g., "schema.table")
// This is common when models need to specify a different schema than the default
if tableSchema, tableOnly := h.parseTableName(tableName); tableSchema != "" {
// Table name includes schema - use it and ignore any other schema providers
logger.Debug("TableName() includes schema: %s.%s", tableSchema, tableOnly)
return tableSchema, tableOnly
}
// Table name is just the table name without schema
// Now determine which schema to use
if schemaProvider, ok := model.(common.SchemaProvider); ok {
schema = schemaProvider.SchemaName()
} else {
schema = defaultSchema
}
return schema, tableName
}
// No TableNameProvider, so check for schema and use entity as table name
if schemaProvider, ok := model.(common.SchemaProvider); ok {
schema = schemaProvider.SchemaName()
} else {
schema = defaultSchema
}
// Default to entity name as table
return schema, entity
}
// getTableName returns the full table name including schema (schema.table)
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
schemaName, tableName := h.getSchemaAndTable(schema, entity, model)
if schemaName != "" {
return fmt.Sprintf("%s.%s", schemaName, tableName)
}
return tableName
}
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
modelType := reflect.TypeOf(model)
// Unwrap pointers, slices, and arrays to get to the base struct type
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
modelType = modelType.Elem()
}
// Validate that we have a struct type
if modelType == nil || modelType.Kind() != reflect.Struct {
logger.Error("Model type must be a struct, got %v for %s.%s", modelType, schema, entity)
return &common.TableMetadata{
Schema: schema,
Table: entity,
Columns: make([]common.Column, 0),
Relations: make([]string, 0),
}
}
metadata := &common.TableMetadata{
Schema: schema,
Table: entity,
Columns: make([]common.Column, 0),
Relations: make([]string, 0),
}
// Generate metadata using reflection (same logic as before)
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
if !field.IsExported() {
continue
}
gormTag := field.Tag.Get("gorm")
jsonTag := field.Tag.Get("json")
if jsonTag == "-" {
continue
}
jsonName := strings.Split(jsonTag, ",")[0]
if jsonName == "" {
jsonName = field.Name
}
if field.Type.Kind() == reflect.Slice ||
(field.Type.Kind() == reflect.Struct && field.Type.Name() != "Time") {
metadata.Relations = append(metadata.Relations, jsonName)
continue
}
column := common.Column{
Name: jsonName,
Type: getColumnType(field),
IsNullable: isNullable(field),
IsPrimary: strings.Contains(gormTag, "primaryKey"),
IsUnique: strings.Contains(gormTag, "unique") || strings.Contains(gormTag, "uniqueIndex"),
HasIndex: strings.Contains(gormTag, "index") || strings.Contains(gormTag, "uniqueIndex"),
}
metadata.Columns = append(metadata.Columns, column)
}
return metadata
}
func (h *Handler) sendResponse(w common.ResponseWriter, data interface{}, metadata *common.Metadata) {
w.SetHeader("Content-Type", "application/json")
err := w.WriteJSON(common.Response{
Success: true,
Data: data,
Metadata: metadata,
})
if err != nil {
logger.Error("Error sending response: %v", err)
}
}
func (h *Handler) sendError(w common.ResponseWriter, status int, code, message string, details interface{}) {
w.SetHeader("Content-Type", "application/json")
w.WriteHeader(status)
err := w.WriteJSON(common.Response{
Success: false,
Error: &common.APIError{
Code: code,
Message: message,
Details: details,
Detail: fmt.Sprintf("%v", details),
},
})
if err != nil {
logger.Error("Error sending response: %v", err)
}
}
// RegisterModel allows registering models at runtime
func (h *Handler) RegisterModel(schema, name string, model interface{}) error {
fullname := fmt.Sprintf("%s.%s", schema, name)
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 _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 {
// Check GORM type tag first
gormTag := field.Tag.Get("gorm")
if strings.Contains(gormTag, "type:") {
parts := strings.Split(gormTag, "type:")
if len(parts) > 1 {
typePart := strings.Split(parts[1], ";")[0]
return typePart
}
}
// Map Go types to SQL types
switch field.Type.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int32:
return "integer"
case reflect.Int64:
return "bigint"
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Bool:
return "boolean"
default:
if field.Type.Name() == "Time" {
return "timestamp"
}
return "unknown"
}
}
func isNullable(field reflect.StructField) bool {
// Check if it's a pointer type
if field.Type.Kind() == reflect.Ptr {
return true
}
// Check if it's a null type from sql package
typeName := field.Type.Name()
if strings.HasPrefix(typeName, "Null") {
return true
}
// Check GORM tags
gormTag := field.Tag.Get("gorm")
return !strings.Contains(gormTag, "not null")
}
// 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
relationType string // "belongsTo", "hasMany", "hasOne", "many2many"
foreignKey string
references string
joinTable string
relatedModel interface{}
}
func (h *Handler) applyPreloads(model interface{}, query common.SelectQuery, preloads []common.PreloadOption) common.SelectQuery {
modelType := reflect.TypeOf(model)
// Unwrap pointers, slices, and arrays to get to the base struct type
for modelType != nil && (modelType.Kind() == reflect.Ptr || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
modelType = modelType.Elem()
}
// Validate that we have a struct type
if modelType == nil || modelType.Kind() != reflect.Struct {
logger.Warn("Cannot apply preloads to non-struct type: %v", modelType)
return query
}
for idx := range preloads {
preload := preloads[idx]
logger.Debug("Processing preload for relation: %s", preload.Relation)
relInfo := h.getRelationshipInfo(modelType, preload.Relation)
if relInfo == nil {
logger.Warn("Relation %s not found in model", preload.Relation)
continue
}
// Use the field name (capitalized) for ORM preloading
// ORMs like GORM and Bun expect the struct field name, not the JSON name
relationFieldName := relInfo.fieldName
// For now, we'll preload without conditions
// TODO: Implement column selection and filtering for preloads
// This requires a more sophisticated approach with callbacks or query builders
// Apply preloading
logger.Debug("Applying preload: %s", preload.Relation)
query = query.PreloadRelation(preload.Relation, func(sq common.SelectQuery) common.SelectQuery {
if len(preload.OmitColumns) > 0 {
allCols := reflection.GetModelColumns(model)
// Remove omitted columns
preload.Columns = []string{}
for _, col := range allCols {
addCols := true
for _, omitCol := range preload.OmitColumns {
if col == omitCol {
addCols = false
break
}
}
if addCols {
preload.Columns = append(preload.Columns, col)
}
}
}
if len(preload.Columns) > 0 {
sq = sq.Column(preload.Columns...)
}
if len(preload.Filters) > 0 {
for _, filter := range preload.Filters {
sq = h.applyFilter(sq, filter)
}
}
if len(preload.Sort) > 0 {
for _, sort := range preload.Sort {
sq = sq.Order(fmt.Sprintf("%s %s", sort.Column, sort.Direction))
}
}
if len(preload.Where) > 0 {
sq = sq.Where(preload.Where)
}
if preload.Limit != nil && *preload.Limit > 0 {
sq = sq.Limit(*preload.Limit)
}
return sq
})
logger.Debug("Applied Preload for relation: %s (field: %s)", preload.Relation, relationFieldName)
}
return query
}
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 ""
}