360 lines
7.9 KiB
Markdown
360 lines
7.9 KiB
Markdown
# ResolveSpec Integration Guide
|
|
|
|
## Overview
|
|
|
|
ResolveSpec is a flexible REST API framework that provides GraphQL-like capabilities while maintaining REST simplicity. It offers two approaches:
|
|
1. **ResolveSpec** - Body-based API with JSON request options
|
|
2. **RestHeadSpec** - Header-based API where query options are passed via HTTP headers
|
|
|
|
For WhatsHooked, we'll use both approaches to provide maximum flexibility.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
go get github.com/bitechdev/ResolveSpec
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### Models
|
|
Models are Go structs that represent database tables. Use GORM tags for database mapping.
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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:
|
|
```json
|
|
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
|
|
|
|
```go
|
|
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:
|
|
```http
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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)
|
|
```http
|
|
X-FieldFilter-Status: active
|
|
X-FieldFilter-Age: 18
|
|
```
|
|
|
|
### Search Operators (RestHeadSpec)
|
|
```http
|
|
X-SearchOp-Gte-Age: 18
|
|
X-SearchOp-Like-Name: john
|
|
```
|
|
|
|
### Body Filters (ResolveSpec)
|
|
```json
|
|
{
|
|
"options": {
|
|
"filters": [
|
|
{"column": "status", "operator": "eq", "value": "active"},
|
|
{"column": "age", "operator": "gte", "value": 18},
|
|
{"column": "name", "operator": "like", "value": "john%"}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Pagination
|
|
|
|
### Offset-Based
|
|
```http
|
|
X-Limit: 50
|
|
X-Offset: 100
|
|
```
|
|
|
|
### Cursor-Based (RestHeadSpec)
|
|
```http
|
|
X-Cursor: eyJpZCI6IjEyMyIsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0=
|
|
X-Limit: 50
|
|
```
|
|
|
|
## Preloading Relationships
|
|
|
|
Load related entities with custom columns:
|
|
|
|
```http
|
|
X-Preload: hooks:id,url,events,posts:id,title
|
|
```
|
|
|
|
```json
|
|
{
|
|
"options": {
|
|
"preload": ["hooks:id,url,events", "posts:id,title"]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Sorting
|
|
|
|
```http
|
|
X-Sort: -created_at,+name
|
|
```
|
|
|
|
Prefix with `-` for descending, `+` for ascending.
|
|
|
|
## Response Formats (RestHeadSpec)
|
|
|
|
### Simple Format (default)
|
|
```http
|
|
X-DetailApi: false
|
|
```
|
|
Returns: `[{...}, {...}]`
|
|
|
|
### Detailed Format
|
|
```http
|
|
X-DetailApi: true
|
|
```
|
|
Returns:
|
|
```json
|
|
{
|
|
"data": [{...}, {...}],
|
|
"meta": {
|
|
"total": 100,
|
|
"limit": 50,
|
|
"offset": 0,
|
|
"cursor": "..."
|
|
}
|
|
}
|
|
```
|
|
|
|
## CORS Configuration
|
|
|
|
```go
|
|
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:
|
|
```json
|
|
{
|
|
"error": "error message",
|
|
"details": "additional context"
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Register models before routes**: Always register all models before calling SetupMuxRoutes
|
|
2. **Use lifecycle hooks**: Implement authentication and validation in hooks
|
|
3. **Schema naming**: Use `schema.table` format consistently
|
|
4. **Transactions**: Use database transactions for multi-record operations
|
|
5. **Validation**: Validate input in OnBeforeCreate/OnBeforeUpdate hooks
|
|
6. **Audit logging**: Use OnAfter* hooks for audit trails
|
|
7. **Performance**: Use preloading instead of N+1 queries
|
|
8. **Security**: Implement row-level security in hooks
|
|
9. **Rate limiting**: Add rate limiting middleware
|
|
10. **Monitoring**: Log all operations for monitoring
|
|
|
|
## Common Patterns
|
|
|
|
### User Filtering (Multi-tenancy)
|
|
```go
|
|
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
|
|
```go
|
|
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
|
|
```go
|
|
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
|