Files
whatshooked/tooldoc/RESOLVESPEC.md
Hein f9773bd07f
Some checks failed
CI / Test (1.23) (push) Failing after -22m46s
CI / Test (1.22) (push) Failing after -22m32s
CI / Build (push) Failing after -23m30s
CI / Lint (push) Failing after -23m12s
refactor(API): Relspect integration
2026-02-05 13:39:43 +02:00

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:

  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

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

  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)

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