mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 17:28:58 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3ba314640 | ||
|
|
93df33e274 | ||
|
|
abd045493a | ||
|
|
a61556d857 |
@@ -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
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ func (h *HTTPRequest) 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 (h *HTTPRequest) UnderlyingRequest() *http.Request {
|
||||||
|
return h.req
|
||||||
|
}
|
||||||
|
|
||||||
// HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter
|
// HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter
|
||||||
type HTTPResponseWriter struct {
|
type HTTPResponseWriter struct {
|
||||||
resp http.ResponseWriter
|
resp http.ResponseWriter
|
||||||
@@ -166,6 +172,12 @@ func (h *HTTPResponseWriter) WriteJSON(data interface{}) error {
|
|||||||
return json.NewEncoder(h.resp).Encode(data)
|
return json.NewEncoder(h.resp).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnderlyingResponseWriter returns the underlying http.ResponseWriter
|
||||||
|
// This is useful when you need to pass the response writer to other handlers
|
||||||
|
func (h *HTTPResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
|
||||||
|
return h.resp
|
||||||
|
}
|
||||||
|
|
||||||
// StandardMuxAdapter creates routes compatible with standard http.HandlerFunc
|
// StandardMuxAdapter creates routes compatible with standard http.HandlerFunc
|
||||||
type StandardMuxAdapter struct {
|
type StandardMuxAdapter struct {
|
||||||
*MuxAdapter
|
*MuxAdapter
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import (
|
|||||||
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FallbackHandler is a function that handles requests when no model is found
|
||||||
|
// It receives the same parameters as the Handle method
|
||||||
|
type FallbackHandler func(w common.ResponseWriter, r common.Request, params map[string]string)
|
||||||
|
|
||||||
// Handler handles API requests using database and model abstractions
|
// Handler handles API requests using database and model abstractions
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
db common.Database
|
db common.Database
|
||||||
registry common.ModelRegistry
|
registry common.ModelRegistry
|
||||||
nestedProcessor *common.NestedCUDProcessor
|
nestedProcessor *common.NestedCUDProcessor
|
||||||
hooks *HookRegistry
|
hooks *HookRegistry
|
||||||
|
fallbackHandler FallbackHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -42,6 +47,12 @@ func (h *Handler) Hooks() *HookRegistry {
|
|||||||
return h.hooks
|
return h.hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||||
|
// If not set, the handler will simply return (pass through to next route)
|
||||||
|
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||||
|
h.fallbackHandler = fallback
|
||||||
|
}
|
||||||
|
|
||||||
// GetDatabase returns the underlying database connection
|
// GetDatabase returns the underlying database connection
|
||||||
// Implements common.SpecHandler interface
|
// Implements common.SpecHandler interface
|
||||||
func (h *Handler) GetDatabase() common.Database {
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
@@ -89,8 +100,14 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
// Get model and populate context with request-scoped data
|
// Get model and populate context with request-scoped data
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Model not found - pass through to next route without writing response
|
// Model not found - call fallback handler if set, otherwise pass through
|
||||||
logger.Debug("Model not found for %s.%s, passing through to next route", schema, entity)
|
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||||
|
if h.fallbackHandler != nil {
|
||||||
|
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||||
|
h.fallbackHandler(w, r, params)
|
||||||
|
} else {
|
||||||
|
logger.Debug("No fallback handler set, passing through to next route")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +173,14 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
|||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Model not found - pass through to next route without writing response
|
// Model not found - call fallback handler if set, otherwise pass through
|
||||||
logger.Debug("Model not found for %s.%s, passing through to next route", schema, entity)
|
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||||
|
if h.fallbackHandler != nil {
|
||||||
|
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||||
|
h.fallbackHandler(w, r, params)
|
||||||
|
} else {
|
||||||
|
logger.Debug("No fallback handler set, passing through to next route")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import (
|
|||||||
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FallbackHandler is a function that handles requests when no model is found
|
||||||
|
// It receives the same parameters as the Handle method
|
||||||
|
type FallbackHandler func(w common.ResponseWriter, r common.Request, params map[string]string)
|
||||||
|
|
||||||
// Handler handles API requests using database and model abstractions
|
// Handler handles API requests using database and model abstractions
|
||||||
// This handler reads filters, columns, and options from HTTP headers
|
// This handler reads filters, columns, and options from HTTP headers
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -24,6 +28,7 @@ type Handler struct {
|
|||||||
registry common.ModelRegistry
|
registry common.ModelRegistry
|
||||||
hooks *HookRegistry
|
hooks *HookRegistry
|
||||||
nestedProcessor *common.NestedCUDProcessor
|
nestedProcessor *common.NestedCUDProcessor
|
||||||
|
fallbackHandler FallbackHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -50,6 +55,12 @@ func (h *Handler) Hooks() *HookRegistry {
|
|||||||
return h.hooks
|
return h.hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||||
|
// If not set, the handler will simply return (pass through to next route)
|
||||||
|
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||||
|
h.fallbackHandler = fallback
|
||||||
|
}
|
||||||
|
|
||||||
// handlePanic is a helper function to handle panics with stack traces
|
// handlePanic is a helper function to handle panics with stack traces
|
||||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||||
stack := debug.Stack()
|
stack := debug.Stack()
|
||||||
@@ -81,8 +92,14 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
// Get model and populate context with request-scoped data
|
// Get model and populate context with request-scoped data
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Model not found - pass through to next route without writing response
|
// Model not found - call fallback handler if set, otherwise pass through
|
||||||
logger.Debug("Model not found for %s.%s, passing through to next route", schema, entity)
|
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||||
|
if h.fallbackHandler != nil {
|
||||||
|
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||||
|
h.fallbackHandler(w, r, params)
|
||||||
|
} else {
|
||||||
|
logger.Debug("No fallback handler set, passing through to next route")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +214,14 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
|||||||
|
|
||||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Model not found - pass through to next route without writing response
|
// Model not found - call fallback handler if set, otherwise pass through
|
||||||
logger.Debug("Model not found for %s.%s, passing through to next route", schema, entity)
|
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||||
|
if h.fallbackHandler != nil {
|
||||||
|
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||||
|
h.fallbackHandler(w, r, params)
|
||||||
|
} else {
|
||||||
|
logger.Debug("No fallback handler set, passing through to next route")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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