mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-30 16:24:26 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ff9a8a24e | ||
|
|
81b87af6e4 |
92
README.md
92
README.md
@@ -13,6 +13,8 @@ Both share the same core architecture and provide dynamic data querying, relatio
|
|||||||
|
|
||||||
**🆕 New in v2.1**: RestHeadSpec (HeaderSpec) - Header-based REST API with lifecycle hooks, cursor pagination, and advanced filtering.
|
**🆕 New in v2.1**: RestHeadSpec (HeaderSpec) - Header-based REST API with lifecycle hooks, cursor pagination, and advanced filtering.
|
||||||
|
|
||||||
|
**🆕 New in v3.0**: Explicit route registration - Routes are now created per registered model for better flexibility and control. OPTIONS method support with full CORS headers for cross-origin requests.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
@@ -65,6 +67,12 @@ Both share the same core architecture and provide dynamic data querying, relatio
|
|||||||
- **🆕 Advanced Filtering**: Field filters, search operators, AND/OR logic, and custom SQL
|
- **🆕 Advanced Filtering**: Field filters, search operators, AND/OR logic, and custom SQL
|
||||||
- **🆕 Base64 Encoding**: Support for base64-encoded header values
|
- **🆕 Base64 Encoding**: Support for base64-encoded header values
|
||||||
|
|
||||||
|
### Routing & CORS (v3.0+)
|
||||||
|
- **🆕 Explicit Route Registration**: Routes created per registered model instead of dynamic lookups
|
||||||
|
- **🆕 OPTIONS Method Support**: Full OPTIONS method support returning model metadata
|
||||||
|
- **🆕 CORS Headers**: Comprehensive CORS support with all HeadSpec headers allowed
|
||||||
|
- **🆕 Better Route Control**: Customize routes per model with more flexibility
|
||||||
|
|
||||||
## API Structure
|
## API Structure
|
||||||
|
|
||||||
### URL Patterns
|
### URL Patterns
|
||||||
@@ -123,13 +131,15 @@ import "github.com/gorilla/mux"
|
|||||||
// Create handler
|
// Create handler
|
||||||
handler := restheadspec.NewHandlerWithGORM(db)
|
handler := restheadspec.NewHandlerWithGORM(db)
|
||||||
|
|
||||||
// Register models using schema.table format
|
// IMPORTANT: Register models BEFORE setting up routes
|
||||||
|
// Routes are created explicitly for each registered model
|
||||||
handler.Registry.RegisterModel("public.users", &User{})
|
handler.Registry.RegisterModel("public.users", &User{})
|
||||||
handler.Registry.RegisterModel("public.posts", &Post{})
|
handler.Registry.RegisterModel("public.posts", &Post{})
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes (creates explicit routes for each registered model)
|
||||||
|
// This replaces the old dynamic route lookup approach
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
restheadspec.SetupMuxRoutes(router, handler)
|
restheadspec.SetupMuxRoutes(router, handler, nil)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
http.ListenAndServe(":8080", router)
|
http.ListenAndServe(":8080", router)
|
||||||
@@ -172,6 +182,42 @@ restheadspec.SetupMuxRoutes(router, handler)
|
|||||||
|
|
||||||
For complete header documentation, see [pkg/restheadspec/HEADERS.md](pkg/restheadspec/HEADERS.md).
|
For complete header documentation, see [pkg/restheadspec/HEADERS.md](pkg/restheadspec/HEADERS.md).
|
||||||
|
|
||||||
|
### CORS & OPTIONS Support
|
||||||
|
|
||||||
|
ResolveSpec and RestHeadSpec include comprehensive CORS support for cross-origin requests:
|
||||||
|
|
||||||
|
**OPTIONS Method**:
|
||||||
|
```http
|
||||||
|
OPTIONS /public/users HTTP/1.1
|
||||||
|
```
|
||||||
|
Returns metadata with appropriate CORS headers:
|
||||||
|
```http
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers: Content-Type, Authorization, X-Select-Fields, X-FieldFilter-*, ...
|
||||||
|
Access-Control-Max-Age: 86400
|
||||||
|
Access-Control-Allow-Credentials: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Features**:
|
||||||
|
- OPTIONS returns model metadata (same as GET metadata endpoint)
|
||||||
|
- All HTTP methods include CORS headers automatically
|
||||||
|
- OPTIONS requests don't require authentication (CORS preflight)
|
||||||
|
- Supports all HeadSpec custom headers (`X-Select-Fields`, `X-FieldFilter-*`, etc.)
|
||||||
|
- 24-hour max age to reduce preflight requests
|
||||||
|
|
||||||
|
**Configuration**:
|
||||||
|
```go
|
||||||
|
import "github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
|
||||||
|
// Get default CORS config
|
||||||
|
corsConfig := common.DefaultCORSConfig()
|
||||||
|
|
||||||
|
// Customize if needed
|
||||||
|
corsConfig.AllowedOrigins = []string{"https://example.com"}
|
||||||
|
corsConfig.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
|
||||||
|
```
|
||||||
|
|
||||||
### Lifecycle Hooks
|
### Lifecycle Hooks
|
||||||
|
|
||||||
RestHeadSpec supports lifecycle hooks for all CRUD operations:
|
RestHeadSpec supports lifecycle hooks for all CRUD operations:
|
||||||
@@ -687,15 +733,16 @@ handler := resolvespec.NewHandler(dbAdapter, registry)
|
|||||||
```go
|
```go
|
||||||
import "github.com/gorilla/mux"
|
import "github.com/gorilla/mux"
|
||||||
|
|
||||||
// Backward compatible way
|
// Register models first
|
||||||
router := mux.NewRouter()
|
handler.Registry.RegisterModel("public.users", &User{})
|
||||||
resolvespec.SetupRoutes(router, handler)
|
handler.Registry.RegisterModel("public.posts", &Post{})
|
||||||
|
|
||||||
// Or manually:
|
// Setup routes - creates explicit routes for each model
|
||||||
router.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
router := mux.NewRouter()
|
||||||
vars := mux.Vars(r)
|
resolvespec.SetupMuxRoutes(router, handler, nil)
|
||||||
handler.Handle(w, r, vars)
|
|
||||||
}).Methods("POST")
|
// Routes created: /public/users, /public/posts, etc.
|
||||||
|
// Each route includes GET, POST, and OPTIONS methods with CORS support
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Gin (Custom Integration)
|
#### Gin (Custom Integration)
|
||||||
@@ -950,7 +997,28 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
### v2.1 (Latest)
|
### v3.0 (Latest - December 2025)
|
||||||
|
|
||||||
|
**Explicit Route Registration (🆕)**:
|
||||||
|
- **Breaking Change**: Routes are now created explicitly for each registered model
|
||||||
|
- **Better Control**: Customize routes per model with more flexibility
|
||||||
|
- **Registration Order**: Models must be registered BEFORE calling SetupMuxRoutes/SetupBunRouterRoutes
|
||||||
|
- **Benefits**: More flexible routing, easier to add custom routes per model, better performance
|
||||||
|
|
||||||
|
**OPTIONS Method & CORS Support (🆕)**:
|
||||||
|
- **OPTIONS Endpoint**: Full OPTIONS method support for CORS preflight requests
|
||||||
|
- **Metadata Response**: OPTIONS returns model metadata (same as GET /metadata)
|
||||||
|
- **CORS Headers**: Comprehensive CORS headers on all responses
|
||||||
|
- **Header Support**: All HeadSpec custom headers (`X-Select-Fields`, `X-FieldFilter-*`, etc.) allowed
|
||||||
|
- **No Auth on OPTIONS**: CORS preflight requests don't require authentication
|
||||||
|
- **Configurable**: Customize CORS settings via `common.CORSConfig`
|
||||||
|
|
||||||
|
**Migration Notes**:
|
||||||
|
- Update your code to register models BEFORE calling SetupMuxRoutes/SetupBunRouterRoutes
|
||||||
|
- Routes like `/public/users` are now created per registered model instead of using dynamic `/{schema}/{entity}` pattern
|
||||||
|
- This is a **breaking change** but provides better control and flexibility
|
||||||
|
|
||||||
|
### v2.1
|
||||||
|
|
||||||
**Recursive CRUD Handler (🆕 Nov 11, 2025)**:
|
**Recursive CRUD Handler (🆕 Nov 11, 2025)**:
|
||||||
- **Nested Object Graphs**: Automatically handle complex object hierarchies with parent-child relationships
|
- **Nested Object Graphs**: Automatically handle complex object hierarchies with parent-child relationships
|
||||||
|
|||||||
@@ -163,8 +163,9 @@ func (h *Handler) SqlQueryList(sqlquery string, pNoCount, pBlankparms, pAllowFil
|
|||||||
// Remove unused input variables
|
// Remove unused input variables
|
||||||
if pBlankparms {
|
if pBlankparms {
|
||||||
for _, kw := range inputvars {
|
for _, kw := range inputvars {
|
||||||
sqlquery = strings.ReplaceAll(sqlquery, kw, "")
|
replacement := getReplacementForBlankParam(sqlquery, kw)
|
||||||
logger.Debug("Removed unused variable: %s", kw)
|
sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
|
||||||
|
logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,8 +502,9 @@ func (h *Handler) SqlQuery(sqlquery string, pBlankparms bool) HTTPFuncType {
|
|||||||
// Remove unused input variables
|
// Remove unused input variables
|
||||||
if pBlankparms {
|
if pBlankparms {
|
||||||
for _, kw := range inputvars {
|
for _, kw := range inputvars {
|
||||||
sqlquery = strings.ReplaceAll(sqlquery, kw, "")
|
replacement := getReplacementForBlankParam(sqlquery, kw)
|
||||||
logger.Debug("Removed unused variable: %s", kw)
|
sqlquery = strings.ReplaceAll(sqlquery, kw, replacement)
|
||||||
|
logger.Debug("Replaced unused variable %s with: %s", kw, replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,6 +872,38 @@ func IsNumeric(s string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getReplacementForBlankParam determines the replacement value for an unused parameter
|
||||||
|
// based on whether it appears within quotes in the SQL query.
|
||||||
|
// It checks for PostgreSQL quotes: single quotes ('') and dollar quotes ($...$)
|
||||||
|
func getReplacementForBlankParam(sqlquery, param string) string {
|
||||||
|
// Find the parameter in the query
|
||||||
|
idx := strings.Index(sqlquery, param)
|
||||||
|
if idx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check characters immediately before and after the parameter
|
||||||
|
var charBefore, charAfter byte
|
||||||
|
|
||||||
|
if idx > 0 {
|
||||||
|
charBefore = sqlquery[idx-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx := idx + len(param)
|
||||||
|
if endIdx < len(sqlquery) {
|
||||||
|
charAfter = sqlquery[endIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parameter is surrounded by quotes (single quote or dollar sign for PostgreSQL dollar-quoted strings)
|
||||||
|
if (charBefore == '\'' || charBefore == '$') && (charAfter == '\'' || charAfter == '$') {
|
||||||
|
// Parameter is in quotes, return empty string
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter is not in quotes, return NULL
|
||||||
|
return "NULL"
|
||||||
|
}
|
||||||
|
|
||||||
// makeResultReceiver creates a slice of interface{} pointers for scanning SQL rows
|
// makeResultReceiver creates a slice of interface{} pointers for scanning SQL rows
|
||||||
// func makeResultReceiver(length int) []interface{} {
|
// func makeResultReceiver(length int) []interface{} {
|
||||||
// result := make([]interface{}, length)
|
// result := make([]interface{}, length)
|
||||||
|
|||||||
@@ -835,3 +835,65 @@ func TestReplaceMetaVariables(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetReplacementForBlankParam tests the blank parameter replacement logic
|
||||||
|
func TestGetReplacementForBlankParam(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqlQuery string
|
||||||
|
param string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Parameter in single quotes",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE name = '[username]'",
|
||||||
|
param: "[username]",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter in dollar quotes",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE data = $[jsondata]$",
|
||||||
|
param: "[jsondata]",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter not in quotes",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE id = [user_id]",
|
||||||
|
param: "[user_id]",
|
||||||
|
expected: "NULL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter not in quotes with AND",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE id = [user_id] AND status = 1",
|
||||||
|
param: "[user_id]",
|
||||||
|
expected: "NULL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter in mixed quote context - before quote",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE id = [user_id] AND name = 'test'",
|
||||||
|
param: "[user_id]",
|
||||||
|
expected: "NULL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter in mixed quote context - in quotes",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE name = '[username]' AND id = 1",
|
||||||
|
param: "[username]",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parameter with dollar quote tag",
|
||||||
|
sqlQuery: "SELECT * FROM users WHERE body = $tag$[content]$tag$",
|
||||||
|
param: "[content]",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := getReplacementForBlankParam(tt.sqlQuery, tt.param)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("Expected replacement '%s', got '%s' for query: %s", tt.expected, result, tt.sqlQuery)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user