Compare commits

...

7 Commits

Author SHA1 Message Date
Hein
32007480c6 Handle cql columns as text by default 2025-12-03 12:18:33 +02:00
Hein
ab1ce869b6 Handling JSON responses in funcspec 2025-12-03 12:10:13 +02:00
Hein
ff72e04428 Added meta operation. 2025-12-03 11:59:58 +02:00
Hein
e35f8a4f14 Fix session id that is an integer. 2025-12-03 11:49:19 +02:00
Hein
5ff9a8a24e Fixed blank params on funcspec 2025-12-03 11:42:32 +02:00
Hein
81b87af6e4 Updated doc 2025-12-03 11:30:59 +02:00
Hein
f3ba314640 Refectored the mux routers. 2025-12-03 10:42:26 +02:00
8 changed files with 892 additions and 214 deletions

View File

@@ -13,6 +13,8 @@ Both share the same core architecture and provide dynamic data querying, relatio
**🆕 New in v2.1**: RestHeadSpec (HeaderSpec) - Header-based REST API with lifecycle hooks, cursor pagination, and advanced filtering. **🆕 New in v2.1**: RestHeadSpec (HeaderSpec) - Header-based REST API with lifecycle hooks, cursor pagination, and advanced filtering.
**🆕 New in v3.0**: Explicit route registration - Routes are now created per registered model for better flexibility and control. OPTIONS method support with full CORS headers for cross-origin requests.
![slogan](./generated_slogan.webp) ![slogan](./generated_slogan.webp)
## Table of Contents ## Table of Contents
@@ -65,6 +67,12 @@ Both share the same core architecture and provide dynamic data querying, relatio
- **🆕 Advanced Filtering**: Field filters, search operators, AND/OR logic, and custom SQL - **🆕 Advanced Filtering**: Field filters, search operators, AND/OR logic, and custom SQL
- **🆕 Base64 Encoding**: Support for base64-encoded header values - **🆕 Base64 Encoding**: Support for base64-encoded header values
### Routing & CORS (v3.0+)
- **🆕 Explicit Route Registration**: Routes created per registered model instead of dynamic lookups
- **🆕 OPTIONS Method Support**: Full OPTIONS method support returning model metadata
- **🆕 CORS Headers**: Comprehensive CORS support with all HeadSpec headers allowed
- **🆕 Better Route Control**: Customize routes per model with more flexibility
## API Structure ## API Structure
### URL Patterns ### URL Patterns
@@ -123,13 +131,15 @@ import "github.com/gorilla/mux"
// Create handler // Create handler
handler := restheadspec.NewHandlerWithGORM(db) handler := restheadspec.NewHandlerWithGORM(db)
// Register models using schema.table format // IMPORTANT: Register models BEFORE setting up routes
// Routes are created explicitly for each registered model
handler.Registry.RegisterModel("public.users", &User{}) handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.posts", &Post{}) handler.Registry.RegisterModel("public.posts", &Post{})
// Setup routes // Setup routes (creates explicit routes for each registered model)
// This replaces the old dynamic route lookup approach
router := mux.NewRouter() router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler) restheadspec.SetupMuxRoutes(router, handler, nil)
// Start server // Start server
http.ListenAndServe(":8080", router) http.ListenAndServe(":8080", router)
@@ -172,6 +182,42 @@ restheadspec.SetupMuxRoutes(router, handler)
For complete header documentation, see [pkg/restheadspec/HEADERS.md](pkg/restheadspec/HEADERS.md). For complete header documentation, see [pkg/restheadspec/HEADERS.md](pkg/restheadspec/HEADERS.md).
### CORS & OPTIONS Support
ResolveSpec and RestHeadSpec include comprehensive CORS support for cross-origin requests:
**OPTIONS Method**:
```http
OPTIONS /public/users HTTP/1.1
```
Returns metadata with appropriate CORS headers:
```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Select-Fields, X-FieldFilter-*, ...
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
```
**Key Features**:
- OPTIONS returns model metadata (same as GET metadata endpoint)
- All HTTP methods include CORS headers automatically
- OPTIONS requests don't require authentication (CORS preflight)
- Supports all HeadSpec custom headers (`X-Select-Fields`, `X-FieldFilter-*`, etc.)
- 24-hour max age to reduce preflight requests
**Configuration**:
```go
import "github.com/bitechdev/ResolveSpec/pkg/common"
// Get default CORS config
corsConfig := common.DefaultCORSConfig()
// Customize if needed
corsConfig.AllowedOrigins = []string{"https://example.com"}
corsConfig.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
```
### Lifecycle Hooks ### Lifecycle Hooks
RestHeadSpec supports lifecycle hooks for all CRUD operations: RestHeadSpec supports lifecycle hooks for all CRUD operations:
@@ -687,15 +733,16 @@ handler := resolvespec.NewHandler(dbAdapter, registry)
```go ```go
import "github.com/gorilla/mux" import "github.com/gorilla/mux"
// Backward compatible way // Register models first
router := mux.NewRouter() handler.Registry.RegisterModel("public.users", &User{})
resolvespec.SetupRoutes(router, handler) handler.Registry.RegisterModel("public.posts", &Post{})
// Or manually: // Setup routes - creates explicit routes for each model
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) { router := mux.NewRouter()
vars := mux.Vars(r) resolvespec.SetupMuxRoutes(router, handler, nil)
handler.Handle(w, r, vars)
}).Methods("POST") // Routes created: /public/users, /public/posts, etc.
// Each route includes GET, POST, and OPTIONS methods with CORS support
``` ```
#### Gin (Custom Integration) #### Gin (Custom Integration)
@@ -950,7 +997,28 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## What's New ## What's New
### v2.1 (Latest) ### v3.0 (Latest - December 2025)
**Explicit Route Registration (🆕)**:
- **Breaking Change**: Routes are now created explicitly for each registered model
- **Better Control**: Customize routes per model with more flexibility
- **Registration Order**: Models must be registered BEFORE calling SetupMuxRoutes/SetupBunRouterRoutes
- **Benefits**: More flexible routing, easier to add custom routes per model, better performance
**OPTIONS Method & CORS Support (🆕)**:
- **OPTIONS Endpoint**: Full OPTIONS method support for CORS preflight requests
- **Metadata Response**: OPTIONS returns model metadata (same as GET /metadata)
- **CORS Headers**: Comprehensive CORS headers on all responses
- **Header Support**: All HeadSpec custom headers (`X-Select-Fields`, `X-FieldFilter-*`, etc.) allowed
- **No Auth on OPTIONS**: CORS preflight requests don't require authentication
- **Configurable**: Customize CORS settings via `common.CORSConfig`
**Migration Notes**:
- Update your code to register models BEFORE calling SetupMuxRoutes/SetupBunRouterRoutes
- Routes like `/public/users` are now created per registered model instead of using dynamic `/{schema}/{entity}` pattern
- This is a **breaking change** but provides better control and flexibility
### v2.1
**Recursive CRUD Handler (🆕 Nov 11, 2025)**: **Recursive CRUD Handler (🆕 Nov 11, 2025)**:
- **Nested Object Graphs**: Automatically handle complex object hierarchies with parent-child relationships - **Nested Object Graphs**: Automatically handle complex object hierarchies with parent-child relationships

119
pkg/common/cors.go Normal file
View File

@@ -0,0 +1,119 @@
package common
import (
"fmt"
"strings"
)
// CORSConfig holds CORS configuration
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
MaxAge int
}
// DefaultCORSConfig returns a default CORS configuration suitable for HeadSpec
func DefaultCORSConfig() CORSConfig {
return CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: GetHeadSpecHeaders(),
MaxAge: 86400, // 24 hours
}
}
// GetHeadSpecHeaders returns all headers used by HeadSpec
func GetHeadSpecHeaders() []string {
return []string{
// Standard headers
"Content-Type",
"Authorization",
"Accept",
"Accept-Language",
"Content-Language",
// Field Selection
"X-Select-Fields",
"X-Not-Select-Fields",
"X-Clean-JSON",
// Filtering & Search
"X-FieldFilter-*",
"X-SearchFilter-*",
"X-SearchOp-*",
"X-SearchOr-*",
"X-SearchAnd-*",
"X-SearchCols",
"X-Custom-SQL-W",
"X-Custom-SQL-W-*",
"X-Custom-SQL-Or",
"X-Custom-SQL-Or-*",
// Joins & Relations
"X-Preload",
"X-Preload-*",
"X-Expand",
"X-Expand-*",
"X-Custom-SQL-Join",
"X-Custom-SQL-Join-*",
// Sorting & Pagination
"X-Sort",
"X-Sort-*",
"X-Limit",
"X-Offset",
"X-Cursor-Forward",
"X-Cursor-Backward",
// Advanced Features
"X-AdvSQL-*",
"X-CQL-Sel-*",
"X-Distinct",
"X-SkipCount",
"X-SkipCache",
"X-Fetch-RowNumber",
"X-PKRow",
// Response Format
"X-SimpleAPI",
"X-DetailAPI",
"X-Syncfusion",
"X-Single-Record-As-Object",
// Transaction Control
"X-Transaction-Atomic",
// X-Files - comprehensive JSON configuration
"X-Files",
}
}
// SetCORSHeaders sets CORS headers on a response writer
func SetCORSHeaders(w ResponseWriter, config CORSConfig) {
// Set allowed origins
if len(config.AllowedOrigins) > 0 {
w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", "))
}
// Set allowed methods
if len(config.AllowedMethods) > 0 {
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
}
// Set allowed headers
if len(config.AllowedHeaders) > 0 {
w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
}
// Set max age
if config.MaxAge > 0 {
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
}
// Allow credentials
w.SetHeader("Access-Control-Allow-Credentials", "true")
// Expose headers that clients can read
w.SetHeader("Access-Control-Expose-Headers", "Content-Range, X-Api-Range-Total, X-Api-Range-Size")
}

View File

@@ -163,8 +163,9 @@ func (h *Handler) SqlQueryList(sqlquery string, pNoCount, pBlankparms, pAllowFil
// Remove unused input variables // Remove unused input variables
if pBlankparms { if pBlankparms {
for _, kw := range inputvars { for _, kw := range inputvars {
sqlquery = strings.ReplaceAll(sqlquery, kw, "") replacement := getReplacementForBlankParam(sqlquery, kw)
logger.Debug("Removed unused variable: %s", kw) sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
} }
} }
@@ -237,7 +238,8 @@ func (h *Handler) SqlQueryList(sqlquery string, pNoCount, pBlankparms, pAllowFil
return err return err
} }
dbobjlist = rows // Normalize PostgreSQL types for proper JSON marshaling
dbobjlist = normalizePostgresTypesList(rows)
if pNoCount { if pNoCount {
total = int64(len(dbobjlist)) total = int64(len(dbobjlist))
@@ -501,8 +503,9 @@ func (h *Handler) SqlQuery(sqlquery string, pBlankparms bool) HTTPFuncType {
// Remove unused input variables // Remove unused input variables
if pBlankparms { if pBlankparms {
for _, kw := range inputvars { for _, kw := range inputvars {
sqlquery = strings.ReplaceAll(sqlquery, kw, "") replacement := getReplacementForBlankParam(sqlquery, kw)
logger.Debug("Removed unused variable: %s", kw) sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
} }
} }
@@ -530,7 +533,7 @@ func (h *Handler) SqlQuery(sqlquery string, pBlankparms bool) HTTPFuncType {
} }
if len(rows) > 0 { if len(rows) > 0 {
dbobj = rows[0] dbobj = normalizePostgresTypes(rows[0])
} }
// Execute AfterSQLExec hook // Execute AfterSQLExec hook
@@ -755,8 +758,8 @@ func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx
} }
if strings.Contains(sqlquery, "[rid_session]") { if strings.Contains(sqlquery, "[rid_session]") {
sessionID := userCtx.SessionID sessionID, _ := strconv.ParseInt(userCtx.SessionID, 10, 64)
sqlquery = strings.ReplaceAll(sqlquery, "[rid_session]", fmt.Sprintf("'%s'", sessionID)) sqlquery = strings.ReplaceAll(sqlquery, "[rid_session]", fmt.Sprintf("%d", sessionID))
} }
if strings.Contains(sqlquery, "[method]") { if strings.Contains(sqlquery, "[method]") {
@@ -870,6 +873,38 @@ func IsNumeric(s string) bool {
return err == nil return err == nil
} }
// getReplacementForBlankParam determines the replacement value for an unused parameter
// based on whether it appears within quotes in the SQL query.
// It checks for PostgreSQL quotes: single quotes (”) and dollar quotes ($...$)
func getReplacementForBlankParam(sqlquery, param string) string {
// Find the parameter in the query
idx := strings.Index(sqlquery, param)
if idx < 0 {
return ""
}
// Check characters immediately before and after the parameter
var charBefore, charAfter byte
if idx > 0 {
charBefore = sqlquery[idx-1]
}
endIdx := idx + len(param)
if endIdx < len(sqlquery) {
charAfter = sqlquery[endIdx]
}
// Check if parameter is surrounded by quotes (single quote or dollar sign for PostgreSQL dollar-quoted strings)
if (charBefore == '\'' || charBefore == '$') && (charAfter == '\'' || charAfter == '$') {
// Parameter is in quotes, return empty string
return ""
}
// Parameter is not in quotes, return NULL
return "NULL"
}
// makeResultReceiver creates a slice of interface{} pointers for scanning SQL rows // makeResultReceiver creates a slice of interface{} pointers for scanning SQL rows
// func makeResultReceiver(length int) []interface{} { // func makeResultReceiver(length int) []interface{} {
// result := make([]interface{}, length) // result := make([]interface{}, length)
@@ -912,3 +947,67 @@ func sendError(w http.ResponseWriter, status int, code, message string, err erro
}) })
_, _ = w.Write(data) _, _ = w.Write(data)
} }
// normalizePostgresTypesList normalizes a list of result maps to handle PostgreSQL types correctly
func normalizePostgresTypesList(rows []map[string]interface{}) []map[string]interface{} {
if len(rows) == 0 {
return rows
}
normalized := make([]map[string]interface{}, len(rows))
for i, row := range rows {
normalized[i] = normalizePostgresTypes(row)
}
return normalized
}
// normalizePostgresTypes normalizes a result map to handle PostgreSQL types correctly for JSON marshaling
// This is necessary because when scanning into map[string]interface{}, PostgreSQL types like jsonb, bytea, etc.
// are scanned as []byte which would be base64-encoded when marshaled to JSON.
func normalizePostgresTypes(row map[string]interface{}) map[string]interface{} {
if row == nil {
return nil
}
normalized := make(map[string]interface{}, len(row))
for key, value := range row {
normalized[key] = normalizePostgresValue(value)
}
return normalized
}
// normalizePostgresValue normalizes a single value to the appropriate Go type for JSON marshaling
func normalizePostgresValue(value interface{}) interface{} {
if value == nil {
return nil
}
switch v := value.(type) {
case []byte:
// Check if it's valid JSON (jsonb type)
// Try to unmarshal as JSON first
var jsonObj interface{}
if err := json.Unmarshal(v, &jsonObj); err == nil {
// It's valid JSON, return as json.RawMessage so it's not double-encoded
return json.RawMessage(v)
}
// Not valid JSON, could be bytea - keep as []byte for base64 encoding
return v
case []interface{}:
// Recursively normalize array elements
normalized := make([]interface{}, len(v))
for i, elem := range v {
normalized[i] = normalizePostgresValue(elem)
}
return normalized
case map[string]interface{}:
// Recursively normalize nested maps
return normalizePostgresTypes(v)
default:
// For other types (int, float, string, bool, etc.), return as-is
return v
}
}

View File

@@ -784,7 +784,7 @@ func TestReplaceMetaVariables(t *testing.T) {
userCtx := &security.UserContext{ userCtx := &security.UserContext{
UserID: 123, UserID: 123,
UserName: "testuser", UserName: "testuser",
SessionID: "session-abc", SessionID: "456",
} }
metainfo := map[string]interface{}{ metainfo := map[string]interface{}{
@@ -819,7 +819,7 @@ func TestReplaceMetaVariables(t *testing.T) {
name: "Replace [rid_session]", name: "Replace [rid_session]",
sqlQuery: "SELECT * FROM sessions WHERE session_id = [rid_session]", sqlQuery: "SELECT * FROM sessions WHERE session_id = [rid_session]",
expectedCheck: func(result string) bool { expectedCheck: func(result string) bool {
return strings.Contains(result, "'session-abc'") return strings.Contains(result, "456")
}, },
}, },
} }
@@ -835,3 +835,65 @@ func TestReplaceMetaVariables(t *testing.T) {
}) })
} }
} }
// TestGetReplacementForBlankParam tests the blank parameter replacement logic
func TestGetReplacementForBlankParam(t *testing.T) {
tests := []struct {
name string
sqlQuery string
param string
expected string
}{
{
name: "Parameter in single quotes",
sqlQuery: "SELECT * FROM users WHERE name = '[username]'",
param: "[username]",
expected: "",
},
{
name: "Parameter in dollar quotes",
sqlQuery: "SELECT * FROM users WHERE data = $[jsondata]$",
param: "[jsondata]",
expected: "",
},
{
name: "Parameter not in quotes",
sqlQuery: "SELECT * FROM users WHERE id = [user_id]",
param: "[user_id]",
expected: "NULL",
},
{
name: "Parameter not in quotes with AND",
sqlQuery: "SELECT * FROM users WHERE id = [user_id] AND status = 1",
param: "[user_id]",
expected: "NULL",
},
{
name: "Parameter in mixed quote context - before quote",
sqlQuery: "SELECT * FROM users WHERE id = [user_id] AND name = 'test'",
param: "[user_id]",
expected: "NULL",
},
{
name: "Parameter in mixed quote context - in quotes",
sqlQuery: "SELECT * FROM users WHERE name = '[username]' AND id = 1",
param: "[username]",
expected: "",
},
{
name: "Parameter with dollar quote tag",
sqlQuery: "SELECT * FROM users WHERE body = $tag$[content]$tag$",
param: "[content]",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getReplacementForBlankParam(tt.sqlQuery, tt.param)
if result != tt.expected {
t.Errorf("Expected replacement '%s', got '%s' for query: %s", tt.expected, result, tt.sqlQuery)
}
})
}
}

View File

@@ -151,6 +151,8 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options) h.handleUpdate(ctx, w, id, req.ID, req.Data, req.Options)
case "delete": case "delete":
h.handleDelete(ctx, w, id, req.Data) h.handleDelete(ctx, w, id, req.Data)
case "meta":
h.handleMeta(ctx, w, schema, entity, model)
default: default:
logger.Error("Invalid operation: %s", req.Operation) logger.Error("Invalid operation: %s", req.Operation)
h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil) h.sendError(w, http.StatusBadRequest, "invalid_operation", "Invalid operation", nil)
@@ -188,6 +190,21 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
h.sendResponse(w, metadata, nil) h.sendResponse(w, metadata, nil)
} }
// handleMeta processes meta operation requests
func (h *Handler) handleMeta(ctx context.Context, w common.ResponseWriter, schema, entity string, model interface{}) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleMeta", err)
}
}()
logger.Info("Getting metadata for %s.%s via meta operation", schema, entity)
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) { func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options common.RequestOptions) {
// Capture panics and return error response // Capture panics and return error response
defer func() { defer func() {

View File

@@ -2,12 +2,14 @@ package resolvespec
import ( import (
"net/http" "net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/router" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/router"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/modelregistry"
@@ -44,39 +46,115 @@ type MiddlewareFunc func(http.Handler) http.Handler
// authMiddleware is optional - if provided, routes will be protected with the middleware // authMiddleware is optional - if provided, routes will be protected with the middleware
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) }) // Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) { func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
// Create handler functions // Get all registered models from the registry
postEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { allModels := handler.registry.GetAllModels()
vars := mux.Vars(r)
reqAdapter := router.NewHTTPRequest(r)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, vars)
})
postEntityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Loop through each registered model and create explicit routes
vars := mux.Vars(r) for fullName := range allModels {
reqAdapter := router.NewHTTPRequest(r) // Parse the full name (e.g., "public.users" or just "users")
respAdapter := router.NewHTTPResponseWriter(w) schema, entity := parseModelName(fullName)
handler.Handle(respAdapter, reqAdapter, vars)
})
getEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Build the route paths
vars := mux.Vars(r) entityPath := buildRoutePath(schema, entity)
reqAdapter := router.NewHTTPRequest(r) entityWithIDPath := buildRoutePath(schema, entity) + "/{id}"
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, vars)
})
// Apply authentication middleware if provided // Create handler functions for this specific entity
if authMiddleware != nil { postEntityHandler := createMuxHandler(handler, schema, entity, "")
postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc) postEntityWithIDHandler := createMuxHandler(handler, schema, entity, "id")
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc) getEntityHandler := createMuxGetHandler(handler, schema, entity, "")
getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc) optionsEntityHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "POST", "OPTIONS"})
optionsEntityWithIDHandler := createMuxOptionsHandler(handler, schema, entity, []string{"POST", "OPTIONS"})
// Apply authentication middleware if provided
if authMiddleware != nil {
postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc)
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc)
getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc)
// Don't apply auth middleware to OPTIONS - CORS preflight must not require auth
}
// Register routes for this entity
muxRouter.Handle(entityPath, postEntityHandler).Methods("POST")
muxRouter.Handle(entityWithIDPath, postEntityWithIDHandler).Methods("POST")
muxRouter.Handle(entityPath, getEntityHandler).Methods("GET")
muxRouter.Handle(entityPath, optionsEntityHandler).Methods("OPTIONS")
muxRouter.Handle(entityWithIDPath, optionsEntityWithIDHandler).Methods("OPTIONS")
} }
}
// Register routes // Helper function to create Mux handler for a specific entity with CORS support
muxRouter.Handle("/{schema}/{entity}", postEntityHandler).Methods("POST") func createMuxHandler(handler *Handler, schema, entity, idParam string) http.HandlerFunc {
muxRouter.Handle("/{schema}/{entity}/{id}", postEntityWithIDHandler).Methods("POST") return func(w http.ResponseWriter, r *http.Request) {
muxRouter.Handle("/{schema}/{entity}", getEntityHandler).Methods("GET") // Set CORS headers
corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
vars := make(map[string]string)
vars["schema"] = schema
vars["entity"] = entity
if idParam != "" {
vars["id"] = mux.Vars(r)[idParam]
}
reqAdapter := router.NewHTTPRequest(r)
handler.Handle(respAdapter, reqAdapter, vars)
}
}
// Helper function to create Mux GET handler for a specific entity with CORS support
func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
vars := make(map[string]string)
vars["schema"] = schema
vars["entity"] = entity
if idParam != "" {
vars["id"] = mux.Vars(r)[idParam]
}
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars)
}
}
// Helper function to create Mux OPTIONS handler that returns metadata
func createMuxOptionsHandler(handler *Handler, schema, entity string, allowedMethods []string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers with the allowed methods for this route
corsConfig := common.DefaultCORSConfig()
corsConfig.AllowedMethods = allowedMethods
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
// Return metadata in the OPTIONS response body
vars := make(map[string]string)
vars["schema"] = schema
vars["entity"] = entity
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars)
}
}
// parseModelName parses a model name like "public.users" into schema and entity
// If no schema is present, returns empty string for schema
func parseModelName(fullName string) (schema, entity string) {
parts := strings.Split(fullName, ".")
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", fullName
}
// buildRoutePath builds a route path from schema and entity
// If schema is empty, returns just "/entity", otherwise "/{schema}/{entity}"
func buildRoutePath(schema, entity string) string {
if schema == "" {
return "/" + entity
}
return "/" + schema + "/" + entity
} }
// Example usage functions for documentation: // Example usage functions for documentation:
@@ -123,51 +201,109 @@ func ExampleWithBun(bunDB *bun.DB) {
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) { func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
r := bunRouter.GetBunRouter() r := bunRouter.GetBunRouter()
r.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { // Get all registered models from the registry
params := map[string]string{ allModels := handler.registry.GetAllModels()
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { // CORS config
params := map[string]string{ corsConfig := common.DefaultCORSConfig()
"schema": req.Param("schema"),
"entity": req.Param("entity"),
"id": req.Param("id"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { // Loop through each registered model and create explicit routes
params := map[string]string{ for fullName := range allModels {
"schema": req.Param("schema"), // Parse the full name (e.g., "public.users" or just "users")
"entity": req.Param("entity"), schema, entity := parseModelName(fullName)
}
reqAdapter := router.NewHTTPRequest(req.Request)
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
r.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { // Build the route paths
params := map[string]string{ entityPath := buildRoutePath(schema, entity)
"schema": req.Param("schema"), entityWithIDPath := entityPath + "/:id"
"entity": req.Param("entity"),
"id": req.Param("id"), // Create closure variables to capture current schema and entity
} currentSchema := schema
reqAdapter := router.NewHTTPRequest(req.Request) currentEntity := entity
respAdapter := router.NewHTTPResponseWriter(w)
handler.HandleGet(respAdapter, reqAdapter, params) // POST route without ID
return nil r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
}) respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
// POST route with ID
r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
// GET route without ID
r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
// GET route with ID
r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
// OPTIONS route without ID (returns metadata)
r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
// OPTIONS route with ID (returns metadata)
r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewHTTPRequest(req.Request)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
}
} }
// ExampleWithBunRouter shows how to use bunrouter from uptrace // ExampleWithBunRouter shows how to use bunrouter from uptrace

View File

@@ -146,13 +146,25 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
h.handleRead(ctx, w, "", options) h.handleRead(ctx, w, "", options)
} }
case "POST": case "POST":
// Create operation // Read request body
body, err := r.Body() body, err := r.Body()
if err != nil { if err != nil {
logger.Error("Failed to read request body: %v", err) logger.Error("Failed to read request body: %v", err)
h.sendError(w, http.StatusBadRequest, "invalid_request", "Failed to read request body", err) h.sendError(w, http.StatusBadRequest, "invalid_request", "Failed to read request body", err)
return return
} }
// Try to detect if this is a meta operation request
var bodyMap map[string]interface{}
if err := json.Unmarshal(body, &bodyMap); err == nil {
if operation, ok := bodyMap["operation"].(string); ok && operation == "meta" {
logger.Info("Detected meta operation request for %s.%s", schema, entity)
h.handleMeta(ctx, w, schema, entity, model)
return
}
}
// Not a meta operation, proceed with normal create/update
var data interface{} var data interface{}
if err := json.Unmarshal(body, &data); err != nil { if err := json.Unmarshal(body, &data); err != nil {
logger.Error("Failed to decode request body: %v", err) logger.Error("Failed to decode request body: %v", err)
@@ -229,6 +241,21 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
h.sendResponse(w, metadata, nil) h.sendResponse(w, metadata, nil)
} }
// handleMeta processes meta operation requests
func (h *Handler) handleMeta(ctx context.Context, w common.ResponseWriter, schema, entity string, model interface{}) {
// Capture panics and return error response
defer func() {
if err := recover(); err != nil {
h.handlePanic(w, "handleMeta", err)
}
}()
logger.Info("Getting metadata for %s.%s via meta operation", schema, entity)
metadata := h.generateMetadata(schema, entity, model)
h.sendResponse(w, metadata, nil)
}
// parseOptionsFromHeaders is now implemented in headers.go // parseOptionsFromHeaders is now implemented in headers.go
func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options ExtendedRequestOptions) { func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id string, options ExtendedRequestOptions) {
@@ -306,7 +333,12 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
if len(options.ComputedQL) > 0 { if len(options.ComputedQL) > 0 {
for colName, colExpr := range options.ComputedQL { for colName, colExpr := range options.ComputedQL {
logger.Debug("Applying computed column: %s", colName) logger.Debug("Applying computed column: %s", colName)
query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", colExpr, colName)) if strings.Contains(colName, "cql") {
query = query.ColumnExpr(fmt.Sprintf("(%s)::text AS %s", colExpr, colName))
} else {
query = query.ColumnExpr(fmt.Sprintf("(%s)AS %s", colExpr, colName))
}
for colIndex := range options.Columns { for colIndex := range options.Columns {
if options.Columns[colIndex] == colName { if options.Columns[colIndex] == colName {
// Remove the computed column from the selected columns to avoid duplication // Remove the computed column from the selected columns to avoid duplication
@@ -320,7 +352,12 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
if len(options.ComputedColumns) > 0 { if len(options.ComputedColumns) > 0 {
for _, cu := range options.ComputedColumns { for _, cu := range options.ComputedColumns {
logger.Debug("Applying computed column: %s", cu.Name) logger.Debug("Applying computed column: %s", cu.Name)
query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", cu.Expression, cu.Name)) if strings.Contains(cu.Name, "cql") {
query = query.ColumnExpr(fmt.Sprintf("(%s)::text AS %s", cu.Expression, cu.Name))
} else {
query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", cu.Expression, cu.Name))
}
for colIndex := range options.Columns { for colIndex := range options.Columns {
if options.Columns[colIndex] == cu.Name { if options.Columns[colIndex] == cu.Name {
// Remove the computed column from the selected columns to avoid duplication // Remove the computed column from the selected columns to avoid duplication

View File

@@ -54,12 +54,14 @@ package restheadspec
import ( import (
"net/http" "net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/uptrace/bunrouter" "github.com/uptrace/bunrouter"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/router" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/router"
"github.com/bitechdev/ResolveSpec/pkg/logger" "github.com/bitechdev/ResolveSpec/pkg/logger"
@@ -97,44 +99,123 @@ type MiddlewareFunc func(http.Handler) http.Handler
// authMiddleware is optional - if provided, routes will be protected with the middleware // authMiddleware is optional - if provided, routes will be protected with the middleware
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) }) // Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) { func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
// Create handler functions // Get all registered models from the registry
entityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { allModels := handler.registry.GetAllModels()
vars := mux.Vars(r)
reqAdapter := router.NewHTTPRequest(r)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, vars)
})
entityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Loop through each registered model and create explicit routes
vars := mux.Vars(r) for fullName := range allModels {
reqAdapter := router.NewHTTPRequest(r) // Parse the full name (e.g., "public.users" or just "users")
respAdapter := router.NewHTTPResponseWriter(w) schema, entity := parseModelName(fullName)
handler.Handle(respAdapter, reqAdapter, vars)
})
metadataHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Build the route paths
vars := mux.Vars(r) entityPath := buildRoutePath(schema, entity)
reqAdapter := router.NewHTTPRequest(r) entityWithIDPath := buildRoutePath(schema, entity) + "/{id}"
respAdapter := router.NewHTTPResponseWriter(w) metadataPath := buildRoutePath(schema, entity) + "/metadata"
handler.HandleGet(respAdapter, reqAdapter, vars)
})
// Apply authentication middleware if provided // Create handler functions for this specific entity
if authMiddleware != nil { entityHandler := createMuxHandler(handler, schema, entity, "")
entityHandler = authMiddleware(entityHandler).(http.HandlerFunc) entityWithIDHandler := createMuxHandler(handler, schema, entity, "id")
entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc) metadataHandler := createMuxGetHandler(handler, schema, entity, "")
metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc) optionsEntityHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "POST", "OPTIONS"})
optionsEntityWithIDHandler := createMuxOptionsHandler(handler, schema, entity, []string{"GET", "PUT", "PATCH", "DELETE", "POST", "OPTIONS"})
// Apply authentication middleware if provided
if authMiddleware != nil {
entityHandler = authMiddleware(entityHandler).(http.HandlerFunc)
entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc)
metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc)
// Don't apply auth middleware to OPTIONS - CORS preflight must not require auth
}
// Register routes for this entity
// GET, POST for /{schema}/{entity}
muxRouter.Handle(entityPath, entityHandler).Methods("GET", "POST")
// GET, PUT, PATCH, DELETE, POST for /{schema}/{entity}/{id}
muxRouter.Handle(entityWithIDPath, entityWithIDHandler).Methods("GET", "PUT", "PATCH", "DELETE", "POST")
// GET for metadata (using HandleGet)
muxRouter.Handle(metadataPath, metadataHandler).Methods("GET")
// OPTIONS for CORS preflight - returns metadata
muxRouter.Handle(entityPath, optionsEntityHandler).Methods("OPTIONS")
muxRouter.Handle(entityWithIDPath, optionsEntityWithIDHandler).Methods("OPTIONS")
} }
}
// Register routes // Helper function to create Mux handler for a specific entity with CORS support
// GET, POST for /{schema}/{entity} func createMuxHandler(handler *Handler, schema, entity, idParam string) http.HandlerFunc {
muxRouter.Handle("/{schema}/{entity}", entityHandler).Methods("GET", "POST") return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
// GET, PUT, PATCH, DELETE, POST for /{schema}/{entity}/{id} vars := make(map[string]string)
muxRouter.Handle("/{schema}/{entity}/{id}", entityWithIDHandler).Methods("GET", "PUT", "PATCH", "DELETE", "POST") vars["schema"] = schema
vars["entity"] = entity
if idParam != "" {
vars["id"] = mux.Vars(r)[idParam]
}
reqAdapter := router.NewHTTPRequest(r)
handler.Handle(respAdapter, reqAdapter, vars)
}
}
// GET for metadata (using HandleGet) // Helper function to create Mux GET handler for a specific entity with CORS support
muxRouter.Handle("/{schema}/{entity}/metadata", metadataHandler).Methods("GET") func createMuxGetHandler(handler *Handler, schema, entity, idParam string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
corsConfig := common.DefaultCORSConfig()
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
vars := make(map[string]string)
vars["schema"] = schema
vars["entity"] = entity
if idParam != "" {
vars["id"] = mux.Vars(r)[idParam]
}
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars)
}
}
// Helper function to create Mux OPTIONS handler that returns metadata
func createMuxOptionsHandler(handler *Handler, schema, entity string, allowedMethods []string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers with the allowed methods for this route
corsConfig := common.DefaultCORSConfig()
corsConfig.AllowedMethods = allowedMethods
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
// Return metadata in the OPTIONS response body
vars := make(map[string]string)
vars["schema"] = schema
vars["entity"] = entity
reqAdapter := router.NewHTTPRequest(r)
handler.HandleGet(respAdapter, reqAdapter, vars)
}
}
// parseModelName parses a model name like "public.users" into schema and entity
// If no schema is present, returns empty string for schema
func parseModelName(fullName string) (schema, entity string) {
parts := strings.Split(fullName, ".")
if len(parts) == 2 {
return parts[0], parts[1]
}
return "", fullName
}
// buildRoutePath builds a route path from schema and entity
// If schema is empty, returns just "/entity", otherwise "/{schema}/{entity}"
func buildRoutePath(schema, entity string) string {
if schema == "" {
return "/" + entity
}
return "/" + schema + "/" + entity
} }
// Example usage functions for documentation: // Example usage functions for documentation:
@@ -181,101 +262,160 @@ func ExampleWithBun(bunDB *bun.DB) {
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) { func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
r := bunRouter.GetBunRouter() r := bunRouter.GetBunRouter()
// GET and POST for /:schema/:entity // Get all registered models from the registry
r.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { allModels := handler.registry.GetAllModels()
params := map[string]string{
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
reqAdapter := router.NewBunRouterRequest(req)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { // CORS config
params := map[string]string{ corsConfig := common.DefaultCORSConfig()
"schema": req.Param("schema"),
"entity": req.Param("entity"),
}
reqAdapter := router.NewBunRouterRequest(req)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
// GET, PUT, PATCH, DELETE for /:schema/:entity/:id // Loop through each registered model and create explicit routes
r.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { for fullName := range allModels {
params := map[string]string{ // Parse the full name (e.g., "public.users" or just "users")
"schema": req.Param("schema"), schema, entity := parseModelName(fullName)
"entity": req.Param("entity"),
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { // Build the route paths
params := map[string]string{ entityPath := buildRoutePath(schema, entity)
"schema": req.Param("schema"), entityWithIDPath := entityPath + "/:id"
"entity": req.Param("entity"), metadataPath := entityPath + "/metadata"
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("PUT", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { // Create closure variables to capture current schema and entity
params := map[string]string{ currentSchema := schema
"schema": req.Param("schema"), currentEntity := entity
"entity": req.Param("entity"),
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
respAdapter := router.NewHTTPResponseWriter(w)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("PATCH", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { // GET and POST for /{schema}/{entity}
params := map[string]string{ r.Handle("GET", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
"schema": req.Param("schema"), respAdapter := router.NewHTTPResponseWriter(w)
"entity": req.Param("entity"), common.SetCORSHeaders(respAdapter, corsConfig)
"id": req.Param("id"), params := map[string]string{
} "schema": currentSchema,
reqAdapter := router.NewBunRouterRequest(req) "entity": currentEntity,
respAdapter := router.NewHTTPResponseWriter(w) }
handler.Handle(respAdapter, reqAdapter, params) reqAdapter := router.NewBunRouterRequest(req)
return nil handler.Handle(respAdapter, reqAdapter, params)
}) return nil
})
r.Handle("DELETE", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("POST", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{ respAdapter := router.NewHTTPResponseWriter(w)
"schema": req.Param("schema"), common.SetCORSHeaders(respAdapter, corsConfig)
"entity": req.Param("entity"), params := map[string]string{
"id": req.Param("id"), "schema": currentSchema,
} "entity": currentEntity,
reqAdapter := router.NewBunRouterRequest(req) }
respAdapter := router.NewHTTPResponseWriter(w) reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params) handler.Handle(respAdapter, reqAdapter, params)
return nil return nil
}) })
// Metadata endpoint // GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}/:id
r.Handle("GET", "/:schema/:entity/metadata", func(w http.ResponseWriter, req bunrouter.Request) error { r.Handle("GET", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
params := map[string]string{ respAdapter := router.NewHTTPResponseWriter(w)
"schema": req.Param("schema"), common.SetCORSHeaders(respAdapter, corsConfig)
"entity": req.Param("entity"), params := map[string]string{
} "schema": currentSchema,
reqAdapter := router.NewBunRouterRequest(req) "entity": currentEntity,
respAdapter := router.NewHTTPResponseWriter(w) "id": req.Param("id"),
handler.HandleGet(respAdapter, reqAdapter, params) }
return nil reqAdapter := router.NewBunRouterRequest(req)
}) handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("POST", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("PUT", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("PATCH", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
r.Handle("DELETE", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
"id": req.Param("id"),
}
reqAdapter := router.NewBunRouterRequest(req)
handler.Handle(respAdapter, reqAdapter, params)
return nil
})
// Metadata endpoint
r.Handle("GET", metadataPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
common.SetCORSHeaders(respAdapter, corsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
// OPTIONS route without ID (returns metadata)
r.Handle("OPTIONS", entityPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
// OPTIONS route with ID (returns metadata)
r.Handle("OPTIONS", entityWithIDPath, func(w http.ResponseWriter, req bunrouter.Request) error {
respAdapter := router.NewHTTPResponseWriter(w)
optionsCorsConfig := corsConfig
optionsCorsConfig.AllowedMethods = []string{"GET", "PUT", "PATCH", "DELETE", "POST", "OPTIONS"}
common.SetCORSHeaders(respAdapter, optionsCorsConfig)
params := map[string]string{
"schema": currentSchema,
"entity": currentEntity,
}
reqAdapter := router.NewBunRouterRequest(req)
handler.HandleGet(respAdapter, reqAdapter, params)
return nil
})
}
} }
// ExampleBunRouterWithBunDB shows usage with both BunRouter and Bun DB // ExampleBunRouterWithBunDB shows usage with both BunRouter and Bun DB