mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-29 15:54:26 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06b2404c0c | ||
|
|
32007480c6 | ||
|
|
ab1ce869b6 | ||
|
|
ff72e04428 | ||
|
|
e35f8a4f14 | ||
|
|
5ff9a8a24e | ||
|
|
81b87af6e4 | ||
|
|
f3ba314640 | ||
|
|
93df33e274 |
92
README.md
92
README.md
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -147,8 +147,11 @@ func (b *BunSelectQuery) Column(columns ...string) common.SelectQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *BunSelectQuery) ColumnExpr(query string, args ...interface{}) common.SelectQuery {
|
func (b *BunSelectQuery) ColumnExpr(query string, args ...interface{}) common.SelectQuery {
|
||||||
b.query = b.query.ColumnExpr(query, args)
|
if len(args) > 0 {
|
||||||
|
b.query = b.query.ColumnExpr(query, args)
|
||||||
|
} else {
|
||||||
|
b.query = b.query.ColumnExpr(query)
|
||||||
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,12 @@ func (g *GormSelectQuery) Column(columns ...string) common.SelectQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *GormSelectQuery) ColumnExpr(query string, args ...interface{}) common.SelectQuery {
|
func (g *GormSelectQuery) ColumnExpr(query string, args ...interface{}) common.SelectQuery {
|
||||||
g.db = g.db.Select(query, args...)
|
if len(args) > 0 {
|
||||||
|
g.db = g.db.Select(query, args...)
|
||||||
|
} else {
|
||||||
|
g.db = g.db.Select(query)
|
||||||
|
}
|
||||||
|
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ func (b *BunRouterRequest) AllHeaders() map[string]string {
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnderlyingRequest returns the underlying *http.Request
|
||||||
|
// This is useful when you need to pass the request to other handlers
|
||||||
|
func (b *BunRouterRequest) UnderlyingRequest() *http.Request {
|
||||||
|
return b.req.Request
|
||||||
|
}
|
||||||
|
|
||||||
// StandardBunRouterAdapter creates routes compatible with standard bunrouter handlers
|
// StandardBunRouterAdapter creates routes compatible with standard bunrouter handlers
|
||||||
type StandardBunRouterAdapter struct {
|
type StandardBunRouterAdapter struct {
|
||||||
*BunRouterAdapter
|
*BunRouterAdapter
|
||||||
|
|||||||
119
pkg/common/cors.go
Normal file
119
pkg/common/cors.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -122,6 +122,7 @@ type Request interface {
|
|||||||
PathParam(key string) string
|
PathParam(key string) string
|
||||||
QueryParam(key string) string
|
QueryParam(key string) string
|
||||||
AllQueryParams() map[string]string // Get all query parameters as a map
|
AllQueryParams() map[string]string // Get all query parameters as a map
|
||||||
|
UnderlyingRequest() *http.Request // Get the underlying *http.Request for forwarding to other handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseWriter interface abstracts HTTP response
|
// ResponseWriter interface abstracts HTTP response
|
||||||
@@ -130,6 +131,7 @@ type ResponseWriter interface {
|
|||||||
WriteHeader(statusCode int)
|
WriteHeader(statusCode int)
|
||||||
Write(data []byte) (int, error)
|
Write(data []byte) (int, error)
|
||||||
WriteJSON(data interface{}) error
|
WriteJSON(data interface{}) error
|
||||||
|
UnderlyingResponseWriter() http.ResponseWriter // Get the underlying http.ResponseWriter for forwarding to other handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPHandlerFunc type for HTTP handlers
|
// HTTPHandlerFunc type for HTTP handlers
|
||||||
@@ -164,6 +166,10 @@ func (s *StandardResponseWriter) WriteJSON(data interface{}) error {
|
|||||||
return json.NewEncoder(s.w).Encode(data)
|
return json.NewEncoder(s.w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StandardResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
|
||||||
|
return s.w
|
||||||
|
}
|
||||||
|
|
||||||
// StandardRequest adapts *http.Request to Request interface
|
// StandardRequest adapts *http.Request to Request interface
|
||||||
type StandardRequest struct {
|
type StandardRequest struct {
|
||||||
r *http.Request
|
r *http.Request
|
||||||
@@ -228,6 +234,10 @@ func (s *StandardRequest) AllQueryParams() map[string]string {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StandardRequest) UnderlyingRequest() *http.Request {
|
||||||
|
return s.r
|
||||||
|
}
|
||||||
|
|
||||||
// TableNameProvider interface for models that provide table names
|
// TableNameProvider interface for models that provide table names
|
||||||
type TableNameProvider interface {
|
type TableNameProvider interface {
|
||||||
TableName() string
|
TableName() string
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package restheadspec
|
package restheadspec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,6 +43,12 @@ func (m *MockRequest) AllQueryParams() map[string]string {
|
|||||||
return m.queryParams
|
return m.queryParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockRequest) UnderlyingRequest() *http.Request {
|
||||||
|
// For testing purposes, return nil
|
||||||
|
// In real scenarios, you might want to construct a proper http.Request
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseOptionsFromQueryParams(t *testing.T) {
|
func TestParseOptionsFromQueryParams(t *testing.T) {
|
||||||
handler := NewHandler(nil, nil)
|
handler := NewHandler(nil, nil)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user