ResolveSpec/pkg/restheadspec/restheadspec.go
2025-12-03 10:42:26 +02:00

440 lines
16 KiB
Go

// Package restheadspec provides the Rest Header Spec API framework.
//
// Rest Header Spec (restheadspec) is a RESTful API framework that reads query options,
// filters, sorting, pagination, and other parameters from HTTP headers instead of
// request bodies or query parameters. This approach provides a clean separation between
// data and metadata in API requests.
//
// # Key Features
//
// - Header-based API configuration: All query options are passed via HTTP headers
// - Database-agnostic: Works with both GORM and Bun ORM through adapters
// - Router-agnostic: Supports multiple HTTP routers (Mux, BunRouter, etc.)
// - Advanced filtering: Supports complex filter operations (eq, gt, lt, like, between, etc.)
// - Pagination and sorting: Built-in support for limit, offset, and multi-column sorting
// - Preloading and expansion: Support for eager loading relationships
// - Multiple response formats: Default, simple, and Syncfusion formats
//
// # HTTP Headers
//
// The following headers are supported for configuring API requests:
//
// - X-Filters: JSON array of filter conditions
// - X-Columns: Comma-separated list of columns to select
// - X-Sort: JSON array of sort specifications
// - X-Limit: Maximum number of records to return
// - X-Offset: Number of records to skip
// - X-Preload: Comma-separated list of relations to preload
// - X-Expand: Comma-separated list of relations to expand (LEFT JOIN)
// - X-Distinct: Boolean to enable DISTINCT queries
// - X-Skip-Count: Boolean to skip total count query
// - X-Response-Format: Response format (detail, simple, syncfusion)
// - X-Clean-JSON: Boolean to remove null/empty fields
// - X-Custom-SQL-Where: Custom SQL WHERE clause (AND)
// - X-Custom-SQL-Or: Custom SQL WHERE clause (OR)
//
// # Usage Example
//
// // Create a handler with GORM
// handler := restheadspec.NewHandlerWithGORM(db)
//
// // Register models
// handler.Registry.RegisterModel("users", User{})
//
// // Setup routes with Mux
// muxRouter := mux.NewRouter()
// restheadspec.SetupMuxRoutes(muxRouter, handler)
//
// // Make a request with headers
// // GET /public/users
// // X-Filters: [{"column":"age","operator":"gt","value":18}]
// // X-Sort: [{"column":"name","direction":"asc"}]
// // X-Limit: 10
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"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
)
// NewHandlerWithGORM creates a new Handler with GORM adapter
func NewHandlerWithGORM(db *gorm.DB) *Handler {
gormAdapter := database.NewGormAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(gormAdapter, registry)
}
// NewHandlerWithBun creates a new Handler with Bun adapter
func NewHandlerWithBun(db *bun.DB) *Handler {
bunAdapter := database.NewBunAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(bunAdapter, registry)
}
// NewStandardMuxRouter creates a router with standard Mux HTTP handlers
func NewStandardMuxRouter() *router.StandardMuxAdapter {
return router.NewStandardMuxAdapter()
}
// NewStandardBunRouter creates a router with standard BunRouter handlers
func NewStandardBunRouter() *router.StandardBunRouterAdapter {
return router.NewStandardBunRouterAdapter()
}
// MiddlewareFunc is a function that wraps an http.Handler with additional functionality
type MiddlewareFunc func(http.Handler) http.Handler
// SetupMuxRoutes sets up routes for the RestHeadSpec API with Mux
// 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) {
// Get all registered models from the registry
allModels := handler.registry.GetAllModels()
// 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)
// Build the route paths
entityPath := buildRoutePath(schema, entity)
entityWithIDPath := buildRoutePath(schema, entity) + "/{id}"
metadataPath := buildRoutePath(schema, entity) + "/metadata"
// 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")
}
}
// 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:
// ExampleWithGORM shows how to use RestHeadSpec with GORM
func ExampleWithGORM(db *gorm.DB) {
// Create handler using GORM
handler := NewHandlerWithGORM(db)
// Setup router without authentication
muxRouter := mux.NewRouter()
SetupMuxRoutes(muxRouter, handler, nil)
// Register models
// handler.registry.RegisterModel("public.users", &User{})
// To add authentication, pass a middleware function:
// import "github.com/bitechdev/ResolveSpec/pkg/security"
// secList := security.NewSecurityList(myProvider)
// authMiddleware := func(h http.Handler) http.Handler {
// return security.NewAuthHandler(secList, h)
// }
// SetupMuxRoutes(muxRouter, handler, authMiddleware)
}
// ExampleWithBun shows how to switch to Bun ORM
func ExampleWithBun(bunDB *bun.DB) {
// Create Bun adapter
dbAdapter := database.NewBunAdapter(bunDB)
// Create model registry
registry := modelregistry.NewModelRegistry()
// registry.RegisterModel("public.users", &User{})
// Create handler
handler := NewHandler(dbAdapter, registry)
// Setup routes without authentication
muxRouter := mux.NewRouter()
SetupMuxRoutes(muxRouter, handler, nil)
}
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
r := bunRouter.GetBunRouter()
// Get all registered models from the registry
allModels := handler.registry.GetAllModels()
// CORS config
corsConfig := common.DefaultCORSConfig()
// 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)
// Build the route paths
entityPath := buildRoutePath(schema, entity)
entityWithIDPath := entityPath + "/:id"
metadataPath := entityPath + "/metadata"
// Create closure variables to capture current schema and entity
currentSchema := schema
currentEntity := entity
// 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("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
})
// 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
func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
// Create handler
handler := NewHandlerWithBun(bunDB)
// Create BunRouter adapter
routerAdapter := NewStandardBunRouter()
// Setup routes
SetupBunRouterRoutes(routerAdapter, handler)
// Get the underlying router for server setup
r := routerAdapter.GetBunRouter()
// Start server
if err := http.ListenAndServe(":8080", r); err != nil {
logger.Error("Server failed to start: %v", err)
}
}