7.9 KiB
7.9 KiB
ResolveSpec Integration Guide
Overview
ResolveSpec is a flexible REST API framework that provides GraphQL-like capabilities while maintaining REST simplicity. It offers two approaches:
- ResolveSpec - Body-based API with JSON request options
- RestHeadSpec - Header-based API where query options are passed via HTTP headers
For WhatsHooked, we'll use both approaches to provide maximum flexibility.
Installation
go get github.com/bitechdev/ResolveSpec
Core Concepts
Models
Models are Go structs that represent database tables. Use GORM tags for database mapping.
type User struct {
ID string `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Registry
The registry maps schema.table names to Go models.
handler := resolvespec.NewHandlerWithGORM(db)
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
Routing
ResolveSpec generates routes automatically for registered models:
/public/users- Collection endpoints/public/users/:id- Individual resource endpoints
ResolveSpec (Body-Based)
Request format:
POST /public/users
{
"operation": "read|create|update|delete",
"data": {
// For create/update operations
},
"options": {
"columns": ["id", "name", "email"],
"filters": [
{"column": "status", "operator": "eq", "value": "active"}
],
"preload": ["hooks:id,url,events"],
"sort": ["-created_at", "+name"],
"limit": 50,
"offset": 0
}
}
Setup with Gorilla Mux
import (
"github.com/gorilla/mux"
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
)
func SetupResolveSpec(db *gorm.DB) *mux.Router {
handler := resolvespec.NewHandlerWithGORM(db)
// Register models
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
// Setup routes
router := mux.NewRouter()
resolvespec.SetupMuxRoutes(router, handler, nil)
return router
}
RestHeadSpec (Header-Based)
Request format:
GET /public/users HTTP/1.1
X-Select-Fields: id,name,email
X-FieldFilter-Status: active
X-Preload: hooks:id,url,events
X-Sort: -created_at,+name
X-Limit: 50
X-Offset: 0
X-DetailApi: true
Setup with Gorilla Mux
import (
"github.com/gorilla/mux"
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
)
func SetupRestHeadSpec(db *gorm.DB) *mux.Router {
handler := restheadspec.NewHandlerWithGORM(db)
// Register models
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
// Setup routes
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)
return router
}
Lifecycle Hooks (RestHeadSpec)
Add hooks for authentication, validation, and audit logging:
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
// Check permissions
userID := ctx.Value("user_id").(string)
if !canRead(userID, req.Schema, req.Entity) {
return fmt.Errorf("unauthorized")
}
return nil
})
handler.OnAfterCreate(func(ctx context.Context, req *restheadspec.Request, result interface{}) error {
// Audit log
log.Info().
Str("user_id", ctx.Value("user_id").(string)).
Str("entity", req.Entity).
Msg("Created record")
return nil
})
Authentication Integration
// Middleware to extract user from JWT
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := ValidateToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", user.ID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Apply to routes
router.Use(AuthMiddleware)
Filtering
Field Filters (RestHeadSpec)
X-FieldFilter-Status: active
X-FieldFilter-Age: 18
Search Operators (RestHeadSpec)
X-SearchOp-Gte-Age: 18
X-SearchOp-Like-Name: john
Body Filters (ResolveSpec)
{
"options": {
"filters": [
{"column": "status", "operator": "eq", "value": "active"},
{"column": "age", "operator": "gte", "value": 18},
{"column": "name", "operator": "like", "value": "john%"}
]
}
}
Pagination
Offset-Based
X-Limit: 50
X-Offset: 100
Cursor-Based (RestHeadSpec)
X-Cursor: eyJpZCI6IjEyMyIsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0=
X-Limit: 50
Preloading Relationships
Load related entities with custom columns:
X-Preload: hooks:id,url,events,posts:id,title
{
"options": {
"preload": ["hooks:id,url,events", "posts:id,title"]
}
}
Sorting
X-Sort: -created_at,+name
Prefix with - for descending, + for ascending.
Response Formats (RestHeadSpec)
Simple Format (default)
X-DetailApi: false
Returns: [{...}, {...}]
Detailed Format
X-DetailApi: true
Returns:
{
"data": [{...}, {...}],
"meta": {
"total": 100,
"limit": 50,
"offset": 0,
"cursor": "..."
}
}
CORS Configuration
corsConfig := &common.CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{"X-Total-Count", "X-Cursor"},
}
restheadspec.SetupMuxRoutes(router, handler, corsConfig)
Error Handling
ResolveSpec returns standard HTTP error codes:
- 200: Success
- 400: Bad Request
- 401: Unauthorized
- 404: Not Found
- 500: Internal Server Error
Error response format:
{
"error": "error message",
"details": "additional context"
}
Best Practices
- Register models before routes: Always register all models before calling SetupMuxRoutes
- Use lifecycle hooks: Implement authentication and validation in hooks
- Schema naming: Use
schema.tableformat consistently - Transactions: Use database transactions for multi-record operations
- Validation: Validate input in OnBeforeCreate/OnBeforeUpdate hooks
- Audit logging: Use OnAfter* hooks for audit trails
- Performance: Use preloading instead of N+1 queries
- Security: Implement row-level security in hooks
- Rate limiting: Add rate limiting middleware
- Monitoring: Log all operations for monitoring
Common Patterns
User Filtering (Multi-tenancy)
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
userID := ctx.Value("user_id").(string)
// Add user_id filter
req.Options.Filters = append(req.Options.Filters, Filter{
Column: "user_id",
Operator: "eq",
Value: userID,
})
return nil
})
Soft Deletes
handler.OnBeforeDelete(func(ctx context.Context, req *restheadspec.Request) error {
// Convert to update with deleted_at
req.Operation = "update"
req.Data = map[string]interface{}{
"deleted_at": time.Now(),
}
return nil
})
Validation
handler.OnBeforeCreate(func(ctx context.Context, req *restheadspec.Request) error {
user := req.Data.(*User)
if user.Email == "" {
return fmt.Errorf("email is required")
}
if !isValidEmail(user.Email) {
return fmt.Errorf("invalid email format")
}
return nil
})
References
- Official Docs: https://github.com/bitechdev/ResolveSpec
- ResolveSpec README: /pkg/resolvespec/README.md
- RestHeadSpec README: /pkg/restheadspec/README.md