refactor(API): ✨ Relspect integration
This commit is contained in:
359
tooldoc/RESOLVESPEC.md
Normal file
359
tooldoc/RESOLVESPEC.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user