From f3ba314640268e404700444e7666ce07bbb96436 Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 3 Dec 2025 10:42:26 +0200 Subject: [PATCH] Refectored the mux routers. --- pkg/common/cors.go | 119 ++++++++++ pkg/resolvespec/resolvespec.go | 276 ++++++++++++++++------ pkg/restheadspec/restheadspec.go | 378 +++++++++++++++++++++---------- 3 files changed, 584 insertions(+), 189 deletions(-) create mode 100644 pkg/common/cors.go diff --git a/pkg/common/cors.go b/pkg/common/cors.go new file mode 100644 index 0000000..8c39fb1 --- /dev/null +++ b/pkg/common/cors.go @@ -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") +} diff --git a/pkg/resolvespec/resolvespec.go b/pkg/resolvespec/resolvespec.go index 715d88d..3d9d734 100644 --- a/pkg/resolvespec/resolvespec.go +++ b/pkg/resolvespec/resolvespec.go @@ -2,12 +2,14 @@ package resolvespec import ( "net/http" + "strings" "github.com/gorilla/mux" "github.com/uptrace/bun" "github.com/uptrace/bunrouter" "gorm.io/gorm" + "github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/router" "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 // Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) }) func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) { - // Create handler functions - postEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, vars) - }) + // Get all registered models from the registry + allModels := handler.registry.GetAllModels() - postEntityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, vars) - }) + // Loop through each registered model and create explicit routes + for fullName := range allModels { + // Parse the full name (e.g., "public.users" or just "users") + schema, entity := parseModelName(fullName) - getEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.HandleGet(respAdapter, reqAdapter, vars) - }) + // Build the route paths + entityPath := buildRoutePath(schema, entity) + entityWithIDPath := buildRoutePath(schema, entity) + "/{id}" - // Apply authentication middleware if provided - if authMiddleware != nil { - postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc) - postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc) - getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc) + // Create handler functions for this specific entity + postEntityHandler := createMuxHandler(handler, schema, entity, "") + postEntityWithIDHandler := createMuxHandler(handler, schema, entity, "id") + getEntityHandler := createMuxGetHandler(handler, schema, entity, "") + 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 - muxRouter.Handle("/{schema}/{entity}", postEntityHandler).Methods("POST") - muxRouter.Handle("/{schema}/{entity}/{id}", postEntityWithIDHandler).Methods("POST") - muxRouter.Handle("/{schema}/{entity}", getEntityHandler).Methods("GET") +// Helper function to create Mux handler for a specific entity with CORS support +func createMuxHandler(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.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: @@ -123,51 +201,109 @@ func ExampleWithBun(bunDB *bun.DB) { func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) { r := bunRouter.GetBunRouter() - r.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - } - reqAdapter := router.NewHTTPRequest(req.Request) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) + // Get all registered models from the registry + allModels := handler.registry.GetAllModels() - r.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "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 - }) + // CORS config + corsConfig := common.DefaultCORSConfig() - r.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - } - reqAdapter := router.NewHTTPRequest(req.Request) - respAdapter := router.NewHTTPResponseWriter(w) - handler.HandleGet(respAdapter, reqAdapter, params) - return nil - }) + // Loop through each registered model and create explicit routes + for fullName := range allModels { + // Parse the full name (e.g., "public.users" or just "users") + schema, entity := parseModelName(fullName) - r.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - "id": req.Param("id"), - } - reqAdapter := router.NewHTTPRequest(req.Request) - respAdapter := router.NewHTTPResponseWriter(w) - handler.HandleGet(respAdapter, reqAdapter, params) - return nil - }) + // Build the route paths + entityPath := buildRoutePath(schema, entity) + entityWithIDPath := entityPath + "/:id" + + // Create closure variables to capture current schema and entity + currentSchema := schema + currentEntity := entity + + // POST route without ID + 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 diff --git a/pkg/restheadspec/restheadspec.go b/pkg/restheadspec/restheadspec.go index 7a19460..f968214 100644 --- a/pkg/restheadspec/restheadspec.go +++ b/pkg/restheadspec/restheadspec.go @@ -54,12 +54,14 @@ package restheadspec import ( "net/http" + "strings" "github.com/gorilla/mux" "github.com/uptrace/bun" "github.com/uptrace/bunrouter" "gorm.io/gorm" + "github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/router" "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 // Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) }) func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) { - // Create handler functions - entityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, vars) - }) + // Get all registered models from the registry + allModels := handler.registry.GetAllModels() - entityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, vars) - }) + // Loop through each registered model and create explicit routes + for fullName := range allModels { + // Parse the full name (e.g., "public.users" or just "users") + schema, entity := parseModelName(fullName) - metadataHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - reqAdapter := router.NewHTTPRequest(r) - respAdapter := router.NewHTTPResponseWriter(w) - handler.HandleGet(respAdapter, reqAdapter, vars) - }) + // Build the route paths + entityPath := buildRoutePath(schema, entity) + entityWithIDPath := buildRoutePath(schema, entity) + "/{id}" + metadataPath := buildRoutePath(schema, entity) + "/metadata" - // Apply authentication middleware if provided - if authMiddleware != nil { - entityHandler = authMiddleware(entityHandler).(http.HandlerFunc) - entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc) - metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc) + // Create handler functions for this specific entity + entityHandler := createMuxHandler(handler, schema, entity, "") + entityWithIDHandler := createMuxHandler(handler, schema, entity, "id") + metadataHandler := createMuxGetHandler(handler, schema, entity, "") + 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 - // GET, POST for /{schema}/{entity} - muxRouter.Handle("/{schema}/{entity}", entityHandler).Methods("GET", "POST") +// Helper function to create Mux handler for a specific entity with CORS support +func createMuxHandler(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) - // GET, PUT, PATCH, DELETE, POST for /{schema}/{entity}/{id} - muxRouter.Handle("/{schema}/{entity}/{id}", entityWithIDHandler).Methods("GET", "PUT", "PATCH", "DELETE", "POST") + 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) + } +} - // GET for metadata (using HandleGet) - muxRouter.Handle("/{schema}/{entity}/metadata", metadataHandler).Methods("GET") +// 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: @@ -181,101 +262,160 @@ func ExampleWithBun(bunDB *bun.DB) { func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) { r := bunRouter.GetBunRouter() - // GET and POST for /:schema/:entity - r.Handle("GET", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { - 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 - }) + // Get all registered models from the registry + allModels := handler.registry.GetAllModels() - r.Handle("POST", "/:schema/:entity", func(w http.ResponseWriter, req bunrouter.Request) error { - 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 - }) + // CORS config + corsConfig := common.DefaultCORSConfig() - // GET, PUT, PATCH, DELETE for /:schema/:entity/:id - r.Handle("GET", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - "id": req.Param("id"), - } - reqAdapter := router.NewBunRouterRequest(req) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) + // Loop through each registered model and create explicit routes + for fullName := range allModels { + // Parse the full name (e.g., "public.users" or just "users") + schema, entity := parseModelName(fullName) - r.Handle("POST", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - "id": req.Param("id"), - } - reqAdapter := router.NewBunRouterRequest(req) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) + // Build the route paths + entityPath := buildRoutePath(schema, entity) + entityWithIDPath := entityPath + "/:id" + metadataPath := entityPath + "/metadata" - r.Handle("PUT", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - "id": req.Param("id"), - } - reqAdapter := router.NewBunRouterRequest(req) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) + // Create closure variables to capture current schema and entity + currentSchema := schema + currentEntity := entity - r.Handle("PATCH", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - "id": req.Param("id"), - } - reqAdapter := router.NewBunRouterRequest(req) - respAdapter := router.NewHTTPResponseWriter(w) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) + // GET and POST for /{schema}/{entity} + 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.NewBunRouterRequest(req) + handler.Handle(respAdapter, reqAdapter, params) + return nil + }) - r.Handle("DELETE", "/:schema/:entity/:id", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "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", 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.NewBunRouterRequest(req) + handler.Handle(respAdapter, reqAdapter, params) + return nil + }) - // Metadata endpoint - r.Handle("GET", "/:schema/:entity/metadata", func(w http.ResponseWriter, req bunrouter.Request) error { - params := map[string]string{ - "schema": req.Param("schema"), - "entity": req.Param("entity"), - } - reqAdapter := router.NewBunRouterRequest(req) - respAdapter := router.NewHTTPResponseWriter(w) - handler.HandleGet(respAdapter, reqAdapter, params) - return nil - }) + // GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}/: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.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