package resolvespec import ( "context" "encoding/json" "fmt" "net/http" "reflect" "runtime/debug" "strings" "github.com/Warky-Devs/ResolveSpec/pkg/common" "github.com/Warky-Devs/ResolveSpec/pkg/logger" ) // Handler handles API requests using database and model abstractions type Handler struct { db common.Database registry common.ModelRegistry } // NewHandler creates a new API handler with database and registry abstractions func NewHandler(db common.Database, registry common.ModelRegistry) *Handler { return &Handler{ db: db, registry: registry, } } // 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) 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...) } // 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.ToLower(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("id = ?", 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) logger.Info("Creating records for %s.%s", schema, entity) query := h.db.NewInsert().Table(tableName) switch v := data.(type) { case map[string]interface{}: 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{}: 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 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) 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) 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) { // 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) logger.Info("Deleting records from %s.%s", schema, entity) 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("id = ?", 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") w.WriteJSON(common.Response{ Success: true, Data: data, Metadata: metadata, }) } func (h *Handler) sendError(w common.ResponseWriter, status int, code, message string, details interface{}) { w.SetHeader("Content-Type", "application/json") w.WriteHeader(status) w.WriteJSON(common.Response{ Success: false, Error: &common.APIError{ Code: code, Message: message, Details: details, Detail: fmt.Sprintf("%v", details), }, }) } // 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) } // 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 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 _, preload := range preloads { 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 query = query.Preload(relationFieldName) 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 "" }