mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-15 12:56:07 +00:00
Updated Database Interface, Added Bun Support
This commit is contained in:
445
pkg/resolvespec/handler.go
Normal file
445
pkg/resolvespec/handler.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package resolvespec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/Warky-Devs/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
// Handler handles API requests using database and model abstractions
|
||||
type Handler struct {
|
||||
db Database
|
||||
registry ModelRegistry
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
func NewHandler(db Database, registry ModelRegistry) *Handler {
|
||||
return &Handler{
|
||||
db: db,
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle processes API requests through router-agnostic interface
|
||||
func (h *Handler) Handle(w ResponseWriter, r Request, params map[string]string) {
|
||||
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 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)
|
||||
|
||||
switch req.Operation {
|
||||
case "read":
|
||||
h.handleRead(ctx, w, schema, entity, id, req.Options)
|
||||
case "create":
|
||||
h.handleCreate(ctx, w, schema, entity, req.Data, req.Options)
|
||||
case "update":
|
||||
h.handleUpdate(ctx, w, schema, entity, id, req.ID, req.Data, req.Options)
|
||||
case "delete":
|
||||
h.handleDelete(ctx, w, schema, entity, 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 ResponseWriter, r Request, params map[string]string) {
|
||||
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 ResponseWriter, schema, entity, id string, options RequestOptions) {
|
||||
logger.Info("Reading records from %s.%s", schema, entity)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
query := h.db.NewSelect().Model(model)
|
||||
|
||||
// Get table name
|
||||
tableName := h.getTableName(schema, entity, model)
|
||||
query = query.Table(tableName)
|
||||
|
||||
// Apply column selection
|
||||
if len(options.Columns) > 0 {
|
||||
logger.Debug("Selecting columns: %v", options.Columns)
|
||||
query = query.Column(options.Columns...)
|
||||
}
|
||||
|
||||
// Note: Preloading is not implemented in the new database abstraction yet
|
||||
// This is a limitation of the current interface design
|
||||
// For now, preloading should use the legacy APIHandler
|
||||
if len(options.Preload) > 0 {
|
||||
logger.Warn("Preloading not yet implemented in new handler - use legacy APIHandler for preload functionality")
|
||||
}
|
||||
|
||||
// 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)
|
||||
singleResult := model
|
||||
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")
|
||||
sliceType := reflect.SliceOf(reflect.TypeOf(model))
|
||||
results := reflect.New(sliceType).Interface()
|
||||
|
||||
if err := query.Scan(ctx, results); err != nil {
|
||||
logger.Error("Error querying records: %v", err)
|
||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
||||
return
|
||||
}
|
||||
result = reflect.ValueOf(results).Elem().Interface()
|
||||
}
|
||||
|
||||
logger.Info("Successfully retrieved records")
|
||||
h.sendResponse(w, result, &Metadata{
|
||||
Total: int64(total),
|
||||
Filtered: int64(total),
|
||||
Limit: optionalInt(options.Limit),
|
||||
Offset: optionalInt(options.Offset),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreate(ctx context.Context, w ResponseWriter, schema, entity string, data interface{}, options RequestOptions) {
|
||||
logger.Info("Creating records for %s.%s", schema, entity)
|
||||
|
||||
tableName := fmt.Sprintf("%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 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 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 ResponseWriter, schema, entity, urlID string, reqID interface{}, data interface{}, options RequestOptions) {
|
||||
logger.Info("Updating records for %s.%s", schema, entity)
|
||||
|
||||
tableName := fmt.Sprintf("%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 ResponseWriter, schema, entity, id string) {
|
||||
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
|
||||
}
|
||||
|
||||
tableName := fmt.Sprintf("%s.%s", schema, entity)
|
||||
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 SelectQuery, filter FilterOption) 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
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||
if provider, ok := model.(TableNameProvider); ok {
|
||||
return provider.TableName()
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", schema, entity)
|
||||
}
|
||||
|
||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) TableMetadata {
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
|
||||
metadata := TableMetadata{
|
||||
Schema: schema,
|
||||
Table: entity,
|
||||
Columns: make([]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 := 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 ResponseWriter, data interface{}, metadata *Metadata) {
|
||||
w.SetHeader("Content-Type", "application/json")
|
||||
w.WriteJSON(Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) sendError(w ResponseWriter, status int, code, message string, details interface{}) {
|
||||
w.SetHeader("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.WriteJSON(Response{
|
||||
Success: false,
|
||||
Error: &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)
|
||||
}
|
||||
Reference in New Issue
Block a user