diff --git a/README.md b/README.md index d23383a..18dcf8c 100644 --- a/README.md +++ b/README.md @@ -19,22 +19,16 @@ Documentation Generated by LLMs * [Features](#features) * [Installation](#installation) * [Quick Start](#quick-start) - * [ResolveSpec (Body-Based API)](#resolvespec-body-based-api) - * [RestHeadSpec (Header-Based API)](#restheadspec-header-based-api) - * [Existing Code (Backward Compatible)](#option-1-existing-code-backward-compatible) - * [New Database-Agnostic API](#option-2-new-database-agnostic-api) - * [Router Integration](#router-integration) + * [ResolveSpec (Body-Based API)](#resolvespec---body-based-api) + * [RestHeadSpec (Header-Based API)](#restheadspec---header-based-api) * [Migration from v1.x](#migration-from-v1x) * [Architecture](#architecture) * [API Structure](#api-structure) -* [RestHeadSpec: Header-Based API](#restheadspec-header-based-api-1) - * [Lifecycle Hooks](#lifecycle-hooks) - * [Cursor Pagination](#cursor-pagination) - * [Response Formats](#response-formats) - * [Single Record as Object](#single-record-as-object-default-behavior) +* [RestHeadSpec Overview](#restheadspec-header-based-api) * [Example Usage](#example-usage) - * [Recursive CRUD Operations](#recursive-crud-operations-) * [Testing](#testing) +* [Additional Packages](#additional-packages) +* [Security Considerations](#security-considerations) * [What's New](#whats-new) ## Features @@ -78,7 +72,7 @@ Documentation Generated by LLMs ### URL Patterns -``` +```text /[schema]/[table_or_entity]/[id] /[schema]/[table_or_entity] /[schema]/[function] @@ -124,509 +118,12 @@ X-Limit: 50 X-DetailApi: true ``` -### Setup with GORM +For complete documentation including setup, headers, lifecycle hooks, cursor pagination, and more, see [pkg/restheadspec/README.md](pkg/restheadspec/README.md). -```Go -import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" -import "github.com/gorilla/mux" - -// Create handler -handler := restheadspec.NewHandlerWithGORM(db) - -// 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.posts", &Post{}) - -// Setup routes (creates explicit routes for each registered model) -// This replaces the old dynamic route lookup approach -router := mux.NewRouter() -restheadspec.SetupMuxRoutes(router, handler, nil) - -// Start server -http.ListenAndServe(":8080", router) -``` - -### Setup with Bun ORM - -```Go -import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" -import "github.com/uptrace/bun" - -// Create handler with Bun -handler := restheadspec.NewHandlerWithBun(bunDB) - -// Register models -handler.Registry.RegisterModel("public.users", &User{}) - -// Setup routes (same as GORM) -router := mux.NewRouter() -restheadspec.SetupMuxRoutes(router, handler) -``` - -### Common Headers - -| Header | Description | Example | -| --------------------------- | -------------------------------------------------- | ------------------------------ | -| `X-Select-Fields` | Columns to include | `id,name,email` | -| `X-Not-Select-Fields` | Columns to exclude | `password,internal_notes` | -| `X-FieldFilter-{col}` | Exact match filter | `X-FieldFilter-Status: active` | -| `X-SearchFilter-{col}` | Fuzzy search (ILIKE) | `X-SearchFilter-Name: john` | -| `X-SearchOp-{op}-{col}` | Filter with operator | `X-SearchOp-Gte-Age: 18` | -| `X-Preload` | Preload relations | `posts:id,title` | -| `X-Sort` | Sort columns | `-created_at,+name` | -| `X-Limit` | Limit results | `50` | -| `X-Offset` | Offset for pagination | `100` | -| `X-Clean-JSON` | Remove null/empty fields | `true` | -| `X-Single-Record-As-Object` | Return single records as objects (default: `true`) | `false` | - -**Available Operators**: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `startswith`, `endswith`, `between`, `betweeninclusive`, `in`, `empty`, `notempty` - -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 - -RestHeadSpec supports lifecycle hooks for all CRUD operations: - -```Go -import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" - -// Create handler -handler := restheadspec.NewHandlerWithGORM(db) - -// Register a before-read hook (e.g., for authorization) -handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error { - // Check permissions - if !userHasPermission(ctx.Context, ctx.Entity) { - return fmt.Errorf("unauthorized access to %s", ctx.Entity) - } - - // Modify query options - ctx.Options.Limit = ptr(100) // Enforce max limit - - return nil -}) - -// Register an after-read hook (e.g., for data transformation) -handler.Hooks.Register(restheadspec.AfterRead, func(ctx *restheadspec.HookContext) error { - // Transform or filter results - if users, ok := ctx.Result.([]User); ok { - for i := range users { - users[i].Email = maskEmail(users[i].Email) - } - } - return nil -}) - -// Register a before-create hook (e.g., for validation) -handler.Hooks.Register(restheadspec.BeforeCreate, func(ctx *restheadspec.HookContext) error { - // Validate data - if user, ok := ctx.Data.(*User); ok { - if user.Email == "" { - return fmt.Errorf("email is required") - } - // Add timestamps - user.CreatedAt = time.Now() - } - return nil -}) -``` - -**Available Hook Types**: - -* `BeforeRead`, `AfterRead` -* `BeforeCreate`, `AfterCreate` -* `BeforeUpdate`, `AfterUpdate` -* `BeforeDelete`, `AfterDelete` - -**HookContext** provides: - -* `Context`: Request context -* `Handler`: Access to handler, database, and registry -* `Schema`, `Entity`, `TableName`: Request info -* `Model`: The registered model type -* `Options`: Parsed request options (filters, sorting, etc.) -* `ID`: Record ID (for single-record operations) -* `Data`: Request data (for create/update) -* `Result`: Operation result (for after hooks) -* `Writer`: Response writer (allows hooks to modify response) - -### Cursor Pagination - -RestHeadSpec supports efficient cursor-based pagination for large datasets: - -```HTTP -GET /public/posts HTTP/1.1 -X-Sort: -created_at,+id -X-Limit: 50 -X-Cursor-Forward: -``` - -**How it works**: - -1. First request returns results + cursor token in response -2. Subsequent requests use `X-Cursor-Forward` or `X-Cursor-Backward` -3. Cursor maintains consistent ordering even with data changes -4. Supports complex multi-column sorting - -**Benefits over offset pagination**: - -* Consistent results when data changes -* Better performance for large offsets -* Prevents "skipped" or duplicate records -* Works with complex sort expressions - -**Example with hooks**: - -```Go -// Enable cursor pagination in a hook -handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error { - // For large tables, enforce cursor pagination - if ctx.Entity == "posts" && ctx.Options.Offset != nil && *ctx.Options.Offset > 1000 { - return fmt.Errorf("use cursor pagination for large offsets") - } - return nil -}) -``` - -### Response Formats - -RestHeadSpec supports multiple response formats: - -**1. Simple Format** (`X-SimpleApi: true`): - -```JSON -[ - { "id": 1, "name": "John" }, - { "id": 2, "name": "Jane" } -] -``` - -**2. Detail Format** (`X-DetailApi: true`, default): - -```JSON -{ - "success": true, - "data": [...], - "metadata": { - "total": 100, - "filtered": 100, - "limit": 50, - "offset": 0 - } -} -``` - -**3. Syncfusion Format** (`X-Syncfusion: true`): - -```JSON -{ - "result": [...], - "count": 100 -} -``` - -### Single Record as Object (Default Behavior) - -By default, RestHeadSpec automatically converts single-element arrays into objects for cleaner API responses. This provides a better developer experience when fetching individual records. - -**Default behavior (enabled)**: - -```HTTP -GET /public/users/123 -``` - -```JSON -{ - "success": true, - "data": { "id": 123, "name": "John", "email": "john@example.com" } -} -``` - -Instead of: - -```JSON -{ - "success": true, - "data": [{ "id": 123, "name": "John", "email": "john@example.com" }] -} -``` - -**To disable** (force arrays for consistency): - -```HTTP -GET /public/users/123 -X-Single-Record-As-Object: false -``` - -```JSON -{ - "success": true, - "data": [{ "id": 123, "name": "John", "email": "john@example.com" }] -} -``` - -**How it works**: - -* When a query returns exactly **one record**, it's returned as an object -* When a query returns **multiple records**, they're returned as an array -* Set `X-Single-Record-As-Object: false` to always receive arrays -* Works with all response formats (simple, detail, syncfusion) -* Applies to both read operations and create/update returning clauses - -**Benefits**: - -* Cleaner API responses for single-record queries -* No need to unwrap single-element arrays on the client side -* Better TypeScript/type inference support -* Consistent with common REST API patterns -* Backward compatible via header opt-out ## Example Usage -### Reading Data with Related Entities - -```JSON -POST /core/users -{ - "operation": "read", - "options": { - "columns": ["id", "name", "email"], - "preload": [ - { - "relation": "posts", - "columns": ["id", "title"], - "filters": [ - { - "column": "status", - "operator": "eq", - "value": "published" - } - ] - } - ], - "filters": [ - { - "column": "active", - "operator": "eq", - "value": true - } - ], - "sort": [ - { - "column": "created_at", - "direction": "desc" - } - ], - "limit": 10, - "offset": 0 - } -} -``` - -### Cursor Pagination (ResolveSpec) - -ResolveSpec now supports cursor-based pagination for efficient traversal of large datasets: - -```JSON -POST /core/posts -{ - "operation": "read", - "options": { - "sort": [ - { - "column": "created_at", - "direction": "desc" - }, - { - "column": "id", - "direction": "asc" - } - ], - "limit": 50, - "cursor_forward": "12345" - } -} -``` - -**How it works**: -1. First request returns results + cursor token (last record's ID) -2. Subsequent requests use `cursor_forward` or `cursor_backward` in options -3. Cursor maintains consistent ordering even when data changes -4. Supports complex multi-column sorting - -**Benefits over offset pagination**: -- Consistent results when data changes between requests -- Better performance for large offsets -- Prevents "skipped" or duplicate records -- Works with complex sort expressions - -**Example request sequence**: - -```JSON -// First request - no cursor -POST /core/posts -{ - "operation": "read", - "options": { - "sort": [{"column": "created_at", "direction": "desc"}], - "limit": 50 - } -} - -// Response includes data + last record ID -// Use the last record's ID as cursor_forward for next page - -// Second request - with cursor -POST /core/posts -{ - "operation": "read", - "options": { - "sort": [{"column": "created_at", "direction": "desc"}], - "limit": 50, - "cursor_forward": "12345" // ID of last record from previous page - } -} - -// For backward pagination -POST /core/posts -{ - "operation": "read", - "options": { - "sort": [{"column": "created_at", "direction": "desc"}], - "limit": 50, - "cursor_backward": "12300" // ID of first record from current page - } -} -``` - -### Recursive CRUD Operations (🆕) - -ResolveSpec now supports automatic handling of nested object graphs with intelligent foreign key resolution. This allows you to create, update, or delete entire object hierarchies in a single request. - -#### Creating Nested Objects - -```JSON -POST /core/users -{ - "operation": "create", - "data": { - "name": "John Doe", - "email": "john@example.com", - "posts": [ - { - "title": "My First Post", - "content": "Hello World", - "tags": [ - {"name": "tech"}, - {"name": "programming"} - ] - }, - { - "title": "Second Post", - "content": "More content" - } - ], - "profile": { - "bio": "Software Developer", - "website": "https://example.com" - } - } -} -``` - -#### Per-Record Operation Control with `_request` - -Control individual operations for each nested record using the special `_request` field: - -```JSON -POST /core/users/123 -{ - "operation": "update", - "data": { - "name": "John Updated", - "posts": [ - { - "_request": "insert", - "title": "New Post", - "content": "Fresh content" - }, - { - "_request": "update", - "id": 456, - "title": "Updated Post Title" - }, - { - "_request": "delete", - "id": 789 - } - ] - } -} -``` - -**Supported** **`_request`** **values**: - -* `insert` - Create a new related record -* `update` - Update an existing related record -* `delete` - Delete a related record -* `upsert` - Create if doesn't exist, update if exists - -#### How It Works - -1. **Automatic Foreign Key Resolution**: Parent IDs are automatically propagated to child records -2. **Recursive Processing**: Handles nested relationships at any depth -3. **Transaction Safety**: All operations execute within database transactions -4. **Relationship Detection**: Automatically detects belongsTo, hasMany, hasOne, and many2many relationships -5. **Flexible Operations**: Mix create, update, and delete operations in a single request - -#### Benefits - -* Reduce API round trips for complex object graphs -* Maintain referential integrity automatically -* Simplify client-side code -* Atomic operations with automatic rollback on errors +For detailed examples of reading data, cursor pagination, recursive CRUD operations, filtering, sorting, and more, see [pkg/resolvespec/README.md](pkg/resolvespec/README.md). ## Installation @@ -644,12 +141,12 @@ ResolveSpec uses JSON request bodies to specify query options: import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" // Create handler -handler := resolvespec.NewAPIHandler(gormDB) -handler.RegisterModel("core", "users", &User{}) +handler := resolvespec.NewHandlerWithGORM(db) +handler.registry.RegisterModel("core.users", &User{}) // Setup routes router := mux.NewRouter() -resolvespec.SetupRoutes(router, handler) +resolvespec.SetupMuxRoutes(router, handler, nil) // Client makes POST request with body: // POST /core/users @@ -663,6 +160,8 @@ resolvespec.SetupRoutes(router, handler) // } ``` +For complete documentation, see [pkg/resolvespec/README.md](pkg/resolvespec/README.md). + ### RestHeadSpec (Header-Based API) RestHeadSpec uses HTTP headers for query options instead of request body: @@ -678,8 +177,8 @@ handler.Registry.RegisterModel("public.users", &User{}) handler.Registry.RegisterModel("public.posts", &Post{}) // Setup routes with Mux -muxRouter := mux.NewRouter() -restheadspec.SetupMuxRoutes(muxRouter, handler) +router := mux.NewRouter() +restheadspec.SetupMuxRoutes(router, handler, nil) // Client makes GET request with headers: // GET /public/users @@ -690,64 +189,17 @@ restheadspec.SetupMuxRoutes(muxRouter, handler) // X-Preload: posts:id,title ``` -See [RestHeadSpec: Header-Based API](#restheadspec-header-based-api-1) for complete header documentation. - -### Option 1: Existing Code (Backward Compatible) - -Your existing code continues to work without any changes: - -```Go -import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" - -// This still works exactly as before -handler := resolvespec.NewAPIHandler(gormDB) -handler.RegisterModel("core", "users", &User{}) -``` +For complete documentation, see [pkg/restheadspec/README.md](pkg/restheadspec/README.md). ## Migration from v1.x -ResolveSpec v2.0 introduces a new database and router abstraction layer while maintaining **100% backward compatibility**. Your existing code will continue to work without any changes. - -### Repository Path Migration - -**IMPORTANT**: The repository has moved from `github.com/Warky-Devs/ResolveSpec` to `github.com/bitechdev/ResolveSpec`. - -To update your imports: - -```Shell -# Update go.mod -go mod edit -replace github.com/Warky-Devs/ResolveSpec=github.com/bitechdev/ResolveSpec@latest -go mod tidy - -# Or update imports manually in your code -# Old: import "github.com/Warky-Devs/ResolveSpec/pkg/resolvespec" -# New: import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" -``` - -Alternatively, use find and replace in your project: - -```Shell -find . -type f -name "*.go" -exec sed -i 's|github.com/Warky-Devs/ResolveSpec|github.com/bitechdev/ResolveSpec|g' {} + -go mod tidy -``` - -### Migration Timeline - -1. **Phase 1**: Update repository path (see above) -2. **Phase 2**: Continue using existing API (no changes needed) -3. **Phase 3**: Gradually adopt new constructors when convenient -4. **Phase 4**: Switch to interface-based approach for new features -5. **Phase 5**: Optionally switch database backends or try RestHeadSpec - -### Detailed Migration Guide - -For detailed migration instructions, examples, and best practices, see [MIGRATION\_GUIDE.md](MIGRATION_GUIDE.md). +ResolveSpec v2.0 maintains **100% backward compatibility**. For detailed migration instructions, see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md). ## Architecture ### Two Complementary APIs -``` +```text ┌─────────────────────────────────────────────────────┐ │ ResolveSpec Framework │ ├─────────────────────┬───────────────────────────────┤ @@ -767,7 +219,7 @@ For detailed migration instructions, examples, and best practices, see [MIGRATIO ### Database Abstraction Layer -``` +```text Your Application Code ↓ Handler (Business Logic) @@ -795,241 +247,13 @@ Your Application Code * **Echo** (manual integration, see examples above) * **Custom Routers** (implement request/response adapters) -### Option 2: New Database-Agnostic API - -#### With GORM (Recommended Migration Path) - -```Go -import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" - -// Create database adapter -dbAdapter := resolvespec.NewGormAdapter(gormDB) - -// Create model registry -registry := resolvespec.NewModelRegistry() -registry.RegisterModel("core.users", &User{}) -registry.RegisterModel("core.posts", &Post{}) - -// Create handler -handler := resolvespec.NewHandler(dbAdapter, registry) -``` - -#### With Bun ORM - -```Go -import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" -import "github.com/uptrace/bun" - -// Create Bun adapter (Bun dependency already included) -dbAdapter := resolvespec.NewBunAdapter(bunDB) - -// Rest is identical to GORM -registry := resolvespec.NewModelRegistry() -handler := resolvespec.NewHandler(dbAdapter, registry) -``` - -### Router Integration - -#### Gorilla Mux (Built-in Support) - -```Go -import "github.com/gorilla/mux" - -// Register models first -handler.Registry.RegisterModel("public.users", &User{}) -handler.Registry.RegisterModel("public.posts", &Post{}) - -// Setup routes - creates explicit routes for each model -router := mux.NewRouter() -resolvespec.SetupMuxRoutes(router, handler, nil) - -// Routes created: /public/users, /public/posts, etc. -// Each route includes GET, POST, and OPTIONS methods with CORS support -``` - -#### Gin (Custom Integration) - -```Go -import "github.com/gin-gonic/gin" - -func setupGin(handler *resolvespec.Handler) *gin.Engine { - r := gin.Default() - - r.POST("/:schema/:entity", func(c *gin.Context) { - params := map[string]string{ - "schema": c.Param("schema"), - "entity": c.Param("entity"), - } - - // Use new adapter interfaces - reqAdapter := resolvespec.NewHTTPRequest(c.Request) - respAdapter := resolvespec.NewHTTPResponseWriter(c.Writer) - handler.Handle(respAdapter, reqAdapter, params) - }) - - return r -} -``` - -#### Echo (Custom Integration) - -```Go -import "github.com/labstack/echo/v4" - -func setupEcho(handler *resolvespec.Handler) *echo.Echo { - e := echo.New() - - e.POST("/:schema/:entity", func(c echo.Context) error { - params := map[string]string{ - "schema": c.Param("schema"), - "entity": c.Param("entity"), - } - - reqAdapter := resolvespec.NewHTTPRequest(c.Request()) - respAdapter := resolvespec.NewHTTPResponseWriter(c.Response().Writer) - handler.Handle(respAdapter, reqAdapter, params) - return nil - }) - - return e -} -``` - -#### BunRouter (Built-in Support) - -```Go -import "github.com/uptrace/bunrouter" - -// Simple setup with built-in function -func setupBunRouter(handler *resolvespec.APIHandlerCompat) *bunrouter.Router { - router := bunrouter.New() - resolvespec.SetupBunRouterWithResolveSpec(router, handler) - return router -} - -// Or use the adapter -func setupBunRouterAdapter() *resolvespec.StandardBunRouterAdapter { - routerAdapter := resolvespec.NewStandardBunRouterAdapter() - - // Register routes manually - routerAdapter.RegisterRouteWithParams("POST", "/:schema/:entity", - []string{"schema", "entity"}, - func(w http.ResponseWriter, r *http.Request, params map[string]string) { - // Your handler logic - }) - - return routerAdapter -} - -// Full uptrace stack (bunrouter + Bun ORM) -func setupFullUptrace(bunDB *bun.DB) *bunrouter.Router { - // Database adapter - dbAdapter := resolvespec.NewBunAdapter(bunDB) - registry := resolvespec.NewModelRegistry() - handler := resolvespec.NewHandler(dbAdapter, registry) - - // Router - router := resolvespec.NewStandardBunRouterAdapter() - resolvespec.SetupBunRouterWithResolveSpec(router.GetBunRouter(), - &resolvespec.APIHandlerCompat{ - newHandler: handler, - }) - - return router.GetBunRouter() -} -``` - -## Configuration - -### Model Registration - -```Go -type User struct { - ID uint `json:"id" gorm:"primaryKey"` - Name string `json:"name"` - Email string `json:"email"` - Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"` -} - -handler.RegisterModel("core", "users", &User{}) -``` - -## Features in Detail - -### Filtering - -Supported operators: - -* eq: Equal -* neq: Not Equal -* gt: Greater Than -* gte: Greater Than or Equal -* lt: Less Than -* lte: Less Than or Equal -* like: LIKE pattern matching -* ilike: Case-insensitive LIKE -* in: IN clause - -### Sorting - -Support for multiple sort criteria with direction: - -```JSON -"sort": [ - { - "column": "created_at", - "direction": "desc" - }, - { - "column": "name", - "direction": "asc" - } -] -``` - -### Computed Columns - -Define virtual columns using SQL expressions: - -```JSON -"computedColumns": [ - { - "name": "full_name", - "expression": "CONCAT(first_name, ' ', last_name)" - } -] -``` - ## Testing -### With New Architecture (Mockable) +ResolveSpec is designed for testability with mockable interfaces. For testing examples and best practices, see the individual package documentation: -```Go -import "github.com/stretchr/testify/mock" - -// Create mock database -type MockDatabase struct { - mock.Mock -} - -func (m *MockDatabase) NewSelect() resolvespec.SelectQuery { - args := m.Called() - return args.Get(0).(resolvespec.SelectQuery) -} - -// Test your handler with mocks -func TestHandler(t *testing.T) { - mockDB := &MockDatabase{} - mockRegistry := resolvespec.NewModelRegistry() - handler := resolvespec.NewHandler(mockDB, mockRegistry) - - // Setup mock expectations - mockDB.On("NewSelect").Return(&MockSelectQuery{}) - - // Test your logic - // ... test code -} -``` +- [ResolveSpec Testing](pkg/resolvespec/README.md#testing) +- [RestHeadSpec Testing](pkg/restheadspec/README.md#testing) +- [WebSocketSpec Testing](pkg/websocketspec/README.md) ## Continuous Integration @@ -1086,6 +310,173 @@ Add this badge to display CI status in your fork: ![Tests](https://github.com/bitechdev/ResolveSpec/workflows/Tests/badge.svg) ``` +## Additional Packages + +ResolveSpec includes several complementary packages that work together to provide a complete web application framework: + +### Core API Packages + +#### ResolveSpec - Body-Based API + +The core body-based REST API with GraphQL-like capabilities. + +**Key Features**: +- JSON request body with operation and options +- Recursive CRUD with nested object support +- Cursor and offset pagination +- Advanced filtering and preloading +- Lifecycle hooks + +For complete documentation, see [pkg/resolvespec/README.md](pkg/resolvespec/README.md). + +#### RestHeadSpec - Header-Based API + +Alternative REST API where query options are passed via HTTP headers. + +**Key Features**: +- All query options via HTTP headers +- Same capabilities as ResolveSpec +- Cleaner separation of data and metadata +- Ideal for GET requests and caching + +For complete documentation, see [pkg/restheadspec/README.md](pkg/restheadspec/README.md). + +#### FuncSpec - Function-Based SQL API + +Execute SQL functions and queries through a simple HTTP API with header-based parameters. + +**Key Features**: +- Direct SQL function invocation +- Header-based parameter passing +- Automatic pagination and counting +- Request/response hooks +- Variable substitution support + +For complete documentation, see [pkg/funcspec/](pkg/funcspec/). + +### Real-Time Communication + +#### WebSocketSpec - WebSocket API + +Real-time bidirectional communication with full CRUD operations and subscriptions. + +**Key Features**: +- Persistent WebSocket connections +- Real-time subscriptions to entity changes +- Automatic push notifications +- Full CRUD with filtering and sorting +- Connection lifecycle management + +For complete documentation, see [pkg/websocketspec/README.md](pkg/websocketspec/README.md). + +#### MQTTSpec - MQTT-Based API + +MQTT-based database operations ideal for IoT and mobile applications. + +**Key Features**: +- Embedded or external MQTT broker support +- QoS 1 (at-least-once delivery) +- Real-time subscriptions +- Multi-tenancy support +- Optimized for unreliable networks + +For complete documentation, see [pkg/mqttspec/README.md](pkg/mqttspec/README.md). + +### Server Components + +#### StaticWeb - Static File Server + +Flexible, interface-driven static file server. + +**Key Features**: +- Router-agnostic with standard `http.Handler` +- Multiple filesystem backends (local, zip, embedded) +- Pluggable cache, MIME, and fallback policies +- Hot-reload support +- 140+ MIME types including modern formats + +**Quick Example**: +```go +import "github.com/bitechdev/ResolveSpec/pkg/server/staticweb" + +service := staticweb.NewService(nil) +provider, _ := staticweb.LocalProvider("./public") + +service.Mount(staticweb.MountConfig{ + URLPrefix: "/", + Provider: provider, + FallbackStrategy: staticweb.HTMLFallback("index.html"), +}) + +router.PathPrefix("/").Handler(service.Handler()) +``` + +For complete documentation, see [pkg/server/staticweb/README.md](pkg/server/staticweb/README.md). + +### Infrastructure & Utilities + +#### Event Broker + +Comprehensive event handling system for real-time event publishing and cross-instance communication. + +**Key Features**: +- Multiple event sources (database, websockets, frontend, system) +- Multiple providers (in-memory, Redis Streams, NATS, PostgreSQL) +- Pattern-based subscriptions +- Automatic CRUD event capture +- Retry logic with exponential backoff +- Prometheus metrics + +For complete documentation, see [pkg/eventbroker/README.md](pkg/eventbroker/README.md). + +#### Cache + +Caching system with support for in-memory and Redis backends. + +For documentation, see [pkg/cache/README.md](pkg/cache/README.md). + +#### Security + +Authentication and authorization framework with hooks integration. + +For documentation, see [pkg/security/README.md](pkg/security/README.md). + +#### Middleware + +HTTP middleware collection for common tasks (CORS, logging, metrics, etc.). + +For documentation, see [pkg/middleware/README.md](pkg/middleware/README.md). + +#### OpenAPI + +OpenAPI/Swagger documentation generation for ResolveSpec APIs. + +For documentation, see [pkg/openapi/README.md](pkg/openapi/README.md). + +#### Metrics + +Prometheus-compatible metrics collection and exposition. + +For documentation, see [pkg/metrics/README.md](pkg/metrics/README.md). + +#### Tracing + +Distributed tracing with OpenTelemetry support. + +For documentation, see [pkg/tracing/README.md](pkg/tracing/README.md). + +#### Error Tracking + +Error tracking and reporting integration. + +For documentation, see [pkg/errortracking/README.md](pkg/errortracking/README.md). + +#### Configuration + +Configuration management with support for multiple formats and environments. + +For documentation, see [pkg/config/README.md](pkg/config/README.md). + ## Security Considerations * Implement proper authentication and authorization diff --git a/pkg/resolvespec/README.md b/pkg/resolvespec/README.md new file mode 100644 index 0000000..ddde8a2 --- /dev/null +++ b/pkg/resolvespec/README.md @@ -0,0 +1,703 @@ +# ResolveSpec - Body-Based REST API + +ResolveSpec provides a REST API where query options are passed in the JSON request body. This approach offers GraphQL-like flexibility while maintaining RESTful principles, making it ideal for complex queries and operations. + +## Features + +* **Body-Based Querying**: All query options passed via JSON request body +* **Lifecycle Hooks**: Before/after hooks for create, read, update, delete operations +* **Cursor Pagination**: Efficient cursor-based pagination with complex sorting +* **Offset Pagination**: Traditional limit/offset pagination support +* **Advanced Filtering**: Multiple operators, AND/OR logic, and custom SQL +* **Relationship Preloading**: Load related entities with custom column selection and filters +* **Recursive CRUD**: Automatically handle nested object graphs with foreign key resolution +* **Computed Columns**: Define virtual columns with SQL expressions +* **Database-Agnostic**: Works with GORM, Bun, or custom database adapters +* **Router-Agnostic**: Integrates with any HTTP router through standard interfaces +* **Type-Safe**: Strong type validation and conversion + +## Quick Start + +### Setup with GORM + +```go +import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" +import "github.com/gorilla/mux" + +// Create handler +handler := resolvespec.NewHandlerWithGORM(db) + +// IMPORTANT: Register models BEFORE setting up routes +handler.registry.RegisterModel("core.users", &User{}) +handler.registry.RegisterModel("core.posts", &Post{}) + +// Setup routes +router := mux.NewRouter() +resolvespec.SetupMuxRoutes(router, handler, nil) + +// Start server +http.ListenAndServe(":8080", router) +``` + +### Setup with Bun ORM + +```go +import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" +import "github.com/uptrace/bun" + +// Create handler with Bun +handler := resolvespec.NewHandlerWithBun(bunDB) + +// Register models +handler.registry.RegisterModel("core.users", &User{}) + +// Setup routes (same as GORM) +router := mux.NewRouter() +resolvespec.SetupMuxRoutes(router, handler, nil) +``` + +## Basic Usage + +### Simple Read Request + +```http +POST /core/users HTTP/1.1 +Content-Type: application/json + +{ + "operation": "read", + "options": { + "columns": ["id", "name", "email"], + "filters": [ + { + "column": "status", + "operator": "eq", + "value": "active" + } + ], + "sort": [ + { + "column": "created_at", + "direction": "desc" + } + ], + "limit": 10, + "offset": 0 + } +} +``` + +### With Preloading + +```http +POST /core/users HTTP/1.1 +Content-Type: application/json + +{ + "operation": "read", + "options": { + "columns": ["id", "name", "email"], + "preload": [ + { + "relation": "posts", + "columns": ["id", "title", "created_at"], + "filters": [ + { + "column": "status", + "operator": "eq", + "value": "published" + } + ] + } + ], + "limit": 10 + } +} +``` + +## Request Structure + +### Request Format + +```json +{ + "operation": "read|create|update|delete", + "data": { + // For create/update operations + }, + "options": { + "columns": [...], + "preload": [...], + "filters": [...], + "sort": [...], + "limit": number, + "offset": number, + "cursor_forward": "string", + "cursor_backward": "string", + "customOperators": [...], + "computedColumns": [...] + } +} +``` + +### Operations + +| Operation | Description | Requires Data | Requires ID | +|-----------|-------------|---------------|-------------| +| `read` | Fetch records | No | Optional (single record) | +| `create` | Create new record(s) | Yes | No | +| `update` | Update existing record(s) | Yes | Yes (in URL) | +| `delete` | Delete record(s) | No | Yes (in URL) | + +### Options Fields + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `columns` | `[]string` | Columns to select | `["id", "name", "email"]` | +| `preload` | `[]PreloadConfig` | Relations to load | See [Preloading](#preloading) | +| `filters` | `[]Filter` | Filter conditions | See [Filtering](#filtering) | +| `sort` | `[]Sort` | Sort criteria | `[{"column": "created_at", "direction": "desc"}]` | +| `limit` | `int` | Max records to return | `50` | +| `offset` | `int` | Number of records to skip | `100` | +| `cursor_forward` | `string` | Cursor for next page | `"12345"` | +| `cursor_backward` | `string` | Cursor for previous page | `"12300"` | +| `customOperators` | `[]CustomOperator` | Custom SQL conditions | See [Custom Operators](#custom-operators) | +| `computedColumns` | `[]ComputedColumn` | Virtual columns | See [Computed Columns](#computed-columns) | + +## Filtering + +### Available Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `eq` | Equal | `{"column": "status", "operator": "eq", "value": "active"}` | +| `neq` | Not Equal | `{"column": "status", "operator": "neq", "value": "deleted"}` | +| `gt` | Greater Than | `{"column": "age", "operator": "gt", "value": 18}` | +| `gte` | Greater Than or Equal | `{"column": "age", "operator": "gte", "value": 18}` | +| `lt` | Less Than | `{"column": "price", "operator": "lt", "value": 100}` | +| `lte` | Less Than or Equal | `{"column": "price", "operator": "lte", "value": 100}` | +| `like` | LIKE pattern | `{"column": "name", "operator": "like", "value": "%john%"}` | +| `ilike` | Case-insensitive LIKE | `{"column": "email", "operator": "ilike", "value": "%@example.com"}` | +| `in` | IN clause | `{"column": "status", "operator": "in", "value": ["active", "pending"]}` | +| `contains` | Contains string | `{"column": "description", "operator": "contains", "value": "important"}` | +| `startswith` | Starts with string | `{"column": "name", "operator": "startswith", "value": "John"}` | +| `endswith` | Ends with string | `{"column": "email", "operator": "endswith", "value": "@example.com"}` | +| `between` | Between (exclusive) | `{"column": "age", "operator": "between", "value": [18, 65]}` | +| `betweeninclusive` | Between (inclusive) | `{"column": "price", "operator": "betweeninclusive", "value": [10, 100]}` | +| `empty` | IS NULL or empty | `{"column": "deleted_at", "operator": "empty"}` | +| `notempty` | IS NOT NULL | `{"column": "email", "operator": "notempty"}` | + +### Complex Filtering Example + +```json +{ + "operation": "read", + "options": { + "filters": [ + { + "column": "status", + "operator": "eq", + "value": "active" + }, + { + "column": "age", + "operator": "gte", + "value": 18 + }, + { + "column": "email", + "operator": "ilike", + "value": "%@company.com" + } + ] + } +} +``` + +## Preloading + +Load related entities with custom configuration: + +```json +{ + "operation": "read", + "options": { + "columns": ["id", "name", "email"], + "preload": [ + { + "relation": "posts", + "columns": ["id", "title", "created_at"], + "filters": [ + { + "column": "status", + "operator": "eq", + "value": "published" + } + ], + "sort": [ + { + "column": "created_at", + "direction": "desc" + } + ], + "limit": 5 + }, + { + "relation": "profile", + "columns": ["bio", "website"] + } + ] + } +} +``` + +## Cursor Pagination + +Efficient pagination for large datasets: + +### First Request (No Cursor) + +```json +{ + "operation": "read", + "options": { + "sort": [ + { + "column": "created_at", + "direction": "desc" + }, + { + "column": "id", + "direction": "asc" + } + ], + "limit": 50 + } +} +``` + +### Next Page (Forward Cursor) + +```json +{ + "operation": "read", + "options": { + "sort": [ + { + "column": "created_at", + "direction": "desc" + }, + { + "column": "id", + "direction": "asc" + } + ], + "limit": 50, + "cursor_forward": "12345" + } +} +``` + +### Previous Page (Backward Cursor) + +```json +{ + "operation": "read", + "options": { + "sort": [ + { + "column": "created_at", + "direction": "desc" + }, + { + "column": "id", + "direction": "asc" + } + ], + "limit": 50, + "cursor_backward": "12300" + } +} +``` + +**Benefits over offset pagination**: +* Consistent results when data changes +* Better performance for large offsets +* Prevents "skipped" or duplicate records +* Works with complex sort expressions + +## Recursive CRUD Operations + +Automatically handle nested object graphs with intelligent foreign key resolution. + +### Creating Nested Objects + +```json +{ + "operation": "create", + "data": { + "name": "John Doe", + "email": "john@example.com", + "posts": [ + { + "title": "My First Post", + "content": "Hello World", + "tags": [ + {"name": "tech"}, + {"name": "programming"} + ] + }, + { + "title": "Second Post", + "content": "More content" + } + ], + "profile": { + "bio": "Software Developer", + "website": "https://example.com" + } + } +} +``` + +### Per-Record Operation Control with `_request` + +Control individual operations for each nested record: + +```json +{ + "operation": "update", + "data": { + "name": "John Updated", + "posts": [ + { + "_request": "insert", + "title": "New Post", + "content": "Fresh content" + }, + { + "_request": "update", + "id": 456, + "title": "Updated Post Title" + }, + { + "_request": "delete", + "id": 789 + } + ] + } +} +``` + +**Supported `_request` values**: +* `insert` - Create a new related record +* `update` - Update an existing related record +* `delete` - Delete a related record +* `upsert` - Create if doesn't exist, update if exists + +**How It Works**: +1. Automatic foreign key resolution - parent IDs propagate to children +2. Recursive processing - handles nested relationships at any depth +3. Transaction safety - all operations execute atomically +4. Relationship detection - automatically detects belongsTo, hasMany, hasOne, many2many +5. Flexible operations - mix create, update, and delete in one request + +## Computed Columns + +Define virtual columns using SQL expressions: + +```json +{ + "operation": "read", + "options": { + "columns": ["id", "first_name", "last_name"], + "computedColumns": [ + { + "name": "full_name", + "expression": "CONCAT(first_name, ' ', last_name)" + }, + { + "name": "age_years", + "expression": "EXTRACT(YEAR FROM AGE(birth_date))" + } + ] + } +} +``` + +## Custom Operators + +Add custom SQL conditions when needed: + +```json +{ + "operation": "read", + "options": { + "customOperators": [ + { + "condition": "LOWER(email) LIKE ?", + "values": ["%@example.com"] + }, + { + "condition": "created_at > NOW() - INTERVAL '7 days'" + } + ] + } +} +``` + +## Lifecycle Hooks + +Register hooks for all CRUD operations: + +```go +import "github.com/bitechdev/ResolveSpec/pkg/resolvespec" + +// Create handler +handler := resolvespec.NewHandlerWithGORM(db) + +// Register a before-read hook (e.g., for authorization) +handler.Hooks().Register(resolvespec.BeforeRead, func(ctx *resolvespec.HookContext) error { + // Check permissions + if !userHasPermission(ctx.Context, ctx.Entity) { + return fmt.Errorf("unauthorized access to %s", ctx.Entity) + } + + // Modify query options + if ctx.Options.Limit == nil || *ctx.Options.Limit > 100 { + ctx.Options.Limit = ptr(100) // Enforce max limit + } + + return nil +}) + +// Register an after-read hook (e.g., for data transformation) +handler.Hooks().Register(resolvespec.AfterRead, func(ctx *resolvespec.HookContext) error { + // Transform or filter results + if users, ok := ctx.Result.([]User); ok { + for i := range users { + users[i].Email = maskEmail(users[i].Email) + } + } + return nil +}) + +// Register a before-create hook (e.g., for validation) +handler.Hooks().Register(resolvespec.BeforeCreate, func(ctx *resolvespec.HookContext) error { + // Validate data + if user, ok := ctx.Data.(*User); ok { + if user.Email == "" { + return fmt.Errorf("email is required") + } + // Add timestamps + user.CreatedAt = time.Now() + } + return nil +}) +``` + +**Available Hook Types**: +* `BeforeRead`, `AfterRead` +* `BeforeCreate`, `AfterCreate` +* `BeforeUpdate`, `AfterUpdate` +* `BeforeDelete`, `AfterDelete` + +**HookContext** provides: +* `Context`: Request context +* `Handler`: Access to handler, database, and registry +* `Schema`, `Entity`, `TableName`: Request info +* `Model`: The registered model type +* `Options`: Parsed request options (filters, sorting, etc.) +* `ID`: Record ID (for single-record operations) +* `Data`: Request data (for create/update) +* `Result`: Operation result (for after hooks) +* `Writer`: Response writer (allows hooks to modify response) + +## Model Registration + +```go +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Email string `json:"email"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"` + Profile *Profile `json:"profile,omitempty" gorm:"foreignKey:UserID"` +} + +type Post struct { + ID uint `json:"id" gorm:"primaryKey"` + UserID uint `json:"user_id"` + Title string `json:"title"` + Content string `json:"content"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + Tags []Tag `json:"tags,omitempty" gorm:"many2many:post_tags"` +} + +// Schema.Table format +handler.registry.RegisterModel("core.users", &User{}) +handler.registry.RegisterModel("core.posts", &Post{}) +``` + +## Complete Example + +```go +package main + +import ( + "log" + "net/http" + + "github.com/bitechdev/ResolveSpec/pkg/resolvespec" + "github.com/gorilla/mux" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Email string `json:"email"` + Status string `json:"status"` + Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"` +} + +type Post struct { + ID uint `json:"id" gorm:"primaryKey"` + UserID uint `json:"user_id"` + Title string `json:"title"` + Content string `json:"content"` + Status string `json:"status"` +} + +func main() { + // Connect to database + db, err := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + + // Create handler + handler := resolvespec.NewHandlerWithGORM(db) + + // Register models + handler.registry.RegisterModel("core.users", &User{}) + handler.registry.RegisterModel("core.posts", &Post{}) + + // Add hooks + handler.Hooks().Register(resolvespec.BeforeRead, func(ctx *resolvespec.HookContext) error { + log.Printf("Reading %s", ctx.Entity) + return nil + }) + + // Setup routes + router := mux.NewRouter() + resolvespec.SetupMuxRoutes(router, handler, nil) + + // Start server + log.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", router)) +} +``` + +## Testing + +ResolveSpec is designed for testability: + +```go +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" +) + +func TestUserRead(t *testing.T) { + handler := resolvespec.NewHandlerWithGORM(testDB) + handler.registry.RegisterModel("core.users", &User{}) + + reqBody := map[string]interface{}{ + "operation": "read", + "options": map[string]interface{}{ + "columns": []string{"id", "name"}, + "limit": 10, + }, + } + + body, _ := json.Marshal(reqBody) + req := httptest.NewRequest("POST", "/core/users", bytes.NewReader(body)) + rec := httptest.NewRecorder() + + // Test your handler... +} +``` + +## Router Integration + +### Gorilla Mux + +```go +router := mux.NewRouter() +resolvespec.SetupMuxRoutes(router, handler, nil) +``` + +### BunRouter + +```go +router := bunrouter.New() +resolvespec.SetupBunRouterWithResolveSpec(router, handler) +``` + +### Custom Routers + +```go +// Implement custom integration using common.Request and common.ResponseWriter +router.POST("/:schema/:entity", func(w http.ResponseWriter, r *http.Request) { + params := extractParams(r) // Your param extraction logic + reqAdapter := router.NewHTTPRequest(r) + respAdapter := router.NewHTTPResponseWriter(w) + handler.Handle(respAdapter, reqAdapter, params) +}) +``` + +## Response Format + +### Success Response + +```json +{ + "success": true, + "data": [...], + "metadata": { + "total": 100, + "filtered": 50, + "limit": 10, + "offset": 0 + } +} +``` + +### Error Response + +```json +{ + "success": false, + "error": { + "code": "validation_error", + "message": "Invalid request", + "details": "..." + } +} +``` + +## See Also + +* [Main README](../../README.md) - ResolveSpec overview +* [RestHeadSpec Package](../restheadspec/README.md) - Header-based API +* [StaticWeb Package](../server/staticweb/README.md) - Static file server + +## License + +This package is part of ResolveSpec and is licensed under the MIT License. diff --git a/pkg/restheadspec/README.md b/pkg/restheadspec/README.md new file mode 100644 index 0000000..38da3e0 --- /dev/null +++ b/pkg/restheadspec/README.md @@ -0,0 +1,445 @@ +# RestHeadSpec - Header-Based REST API + +RestHeadSpec provides a REST API where all query options are passed via HTTP headers instead of the request body. This provides cleaner separation between data and metadata, making it ideal for GET requests and RESTful architectures. + +## Features + +* **Header-Based Querying**: All query options via HTTP headers +* **Lifecycle Hooks**: Before/after hooks for create, read, update, delete operations +* **Cursor Pagination**: Efficient cursor-based pagination with complex sorting +* **Advanced Filtering**: Field filters, search operators, AND/OR logic +* **Multiple Response Formats**: Simple, detailed, and Syncfusion-compatible responses +* **Single Record as Object**: Automatically return single-element arrays as objects (default) +* **Base64 Support**: Base64-encoded header values for complex queries +* **Type-Aware Filtering**: Automatic type detection and conversion +* **CORS Support**: Comprehensive CORS headers for cross-origin requests +* **OPTIONS Method**: Full OPTIONS support for CORS preflight + +## Quick Start + +### Setup with GORM + +```go +import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" +import "github.com/gorilla/mux" + +// Create handler +handler := restheadspec.NewHandlerWithGORM(db) + +// IMPORTANT: Register models BEFORE setting up routes +handler.Registry.RegisterModel("public.users", &User{}) +handler.Registry.RegisterModel("public.posts", &Post{}) + +// Setup routes +router := mux.NewRouter() +restheadspec.SetupMuxRoutes(router, handler, nil) + +// Start server +http.ListenAndServe(":8080", router) +``` + +### Setup with Bun ORM + +```go +import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" +import "github.com/uptrace/bun" + +// Create handler with Bun +handler := restheadspec.NewHandlerWithBun(bunDB) + +// Register models +handler.Registry.RegisterModel("public.users", &User{}) + +// Setup routes (same as GORM) +router := mux.NewRouter() +restheadspec.SetupMuxRoutes(router, handler, nil) +``` + +## Basic Usage + +### Simple GET Request + +```http +GET /public/users HTTP/1.1 +Host: api.example.com +X-Select-Fields: id,name,email +X-FieldFilter-Status: active +X-Sort: -created_at +X-Limit: 50 +``` + +### With Preloading + +```http +GET /public/users HTTP/1.1 +X-Select-Fields: id,name,email,department_id +X-Preload: department:id,name +X-FieldFilter-Status: active +X-Limit: 50 +``` + +## Common Headers + +| Header | Description | Example | +|--------|-------------|---------| +| `X-Select-Fields` | Columns to include | `id,name,email` | +| `X-Not-Select-Fields` | Columns to exclude | `password,internal_notes` | +| `X-FieldFilter-{col}` | Exact match filter | `X-FieldFilter-Status: active` | +| `X-SearchFilter-{col}` | Fuzzy search (ILIKE) | `X-SearchFilter-Name: john` | +| `X-SearchOp-{op}-{col}` | Filter with operator | `X-SearchOp-Gte-Age: 18` | +| `X-Preload` | Preload relations | `posts:id,title` | +| `X-Sort` | Sort columns | `-created_at,+name` | +| `X-Limit` | Limit results | `50` | +| `X-Offset` | Offset for pagination | `100` | +| `X-Clean-JSON` | Remove null/empty fields | `true` | +| `X-Single-Record-As-Object` | Return single records as objects | `false` | + +**Available Operators**: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `startswith`, `endswith`, `between`, `betweeninclusive`, `in`, `empty`, `notempty` + +For complete header documentation, see [HEADERS.md](HEADERS.md). + +## Lifecycle Hooks + +RestHeadSpec supports lifecycle hooks for all CRUD operations: + +```go +import "github.com/bitechdev/ResolveSpec/pkg/restheadspec" + +// Create handler +handler := restheadspec.NewHandlerWithGORM(db) + +// Register a before-read hook (e.g., for authorization) +handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error { + // Check permissions + if !userHasPermission(ctx.Context, ctx.Entity) { + return fmt.Errorf("unauthorized access to %s", ctx.Entity) + } + + // Modify query options + ctx.Options.Limit = ptr(100) // Enforce max limit + + return nil +}) + +// Register an after-read hook (e.g., for data transformation) +handler.Hooks.Register(restheadspec.AfterRead, func(ctx *restheadspec.HookContext) error { + // Transform or filter results + if users, ok := ctx.Result.([]User); ok { + for i := range users { + users[i].Email = maskEmail(users[i].Email) + } + } + return nil +}) + +// Register a before-create hook (e.g., for validation) +handler.Hooks.Register(restheadspec.BeforeCreate, func(ctx *restheadspec.HookContext) error { + // Validate data + if user, ok := ctx.Data.(*User); ok { + if user.Email == "" { + return fmt.Errorf("email is required") + } + // Add timestamps + user.CreatedAt = time.Now() + } + return nil +}) +``` + +**Available Hook Types**: +* `BeforeRead`, `AfterRead` +* `BeforeCreate`, `AfterCreate` +* `BeforeUpdate`, `AfterUpdate` +* `BeforeDelete`, `AfterDelete` + +**HookContext** provides: +* `Context`: Request context +* `Handler`: Access to handler, database, and registry +* `Schema`, `Entity`, `TableName`: Request info +* `Model`: The registered model type +* `Options`: Parsed request options (filters, sorting, etc.) +* `ID`: Record ID (for single-record operations) +* `Data`: Request data (for create/update) +* `Result`: Operation result (for after hooks) +* `Writer`: Response writer (allows hooks to modify response) + +## Cursor Pagination + +RestHeadSpec supports efficient cursor-based pagination for large datasets: + +```http +GET /public/posts HTTP/1.1 +X-Sort: -created_at,+id +X-Limit: 50 +X-Cursor-Forward: +``` + +**How it works**: +1. First request returns results + cursor token in response +2. Subsequent requests use `X-Cursor-Forward` or `X-Cursor-Backward` +3. Cursor maintains consistent ordering even with data changes +4. Supports complex multi-column sorting + +**Benefits over offset pagination**: +* Consistent results when data changes +* Better performance for large offsets +* Prevents "skipped" or duplicate records +* Works with complex sort expressions + +**Example with hooks**: + +```go +// Enable cursor pagination in a hook +handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error { + // For large tables, enforce cursor pagination + if ctx.Entity == "posts" && ctx.Options.Offset != nil && *ctx.Options.Offset > 1000 { + return fmt.Errorf("use cursor pagination for large offsets") + } + return nil +}) +``` + +## Response Formats + +RestHeadSpec supports multiple response formats: + +**1. Simple Format** (`X-SimpleApi: true`): + +```json +[ + { "id": 1, "name": "John" }, + { "id": 2, "name": "Jane" } +] +``` + +**2. Detail Format** (`X-DetailApi: true`, default): + +```json +{ + "success": true, + "data": [...], + "metadata": { + "total": 100, + "filtered": 100, + "limit": 50, + "offset": 0 + } +} +``` + +**3. Syncfusion Format** (`X-Syncfusion: true`): + +```json +{ + "result": [...], + "count": 100 +} +``` + +## Single Record as Object (Default Behavior) + +By default, RestHeadSpec automatically converts single-element arrays into objects for cleaner API responses. + +**Default behavior (enabled)**: + +```http +GET /public/users/123 +``` + +```json +{ + "success": true, + "data": { "id": 123, "name": "John", "email": "john@example.com" } +} +``` + +**To disable** (force arrays): + +```http +GET /public/users/123 +X-Single-Record-As-Object: false +``` + +```json +{ + "success": true, + "data": [{ "id": 123, "name": "John", "email": "john@example.com" }] +} +``` + +**How it works**: +* When a query returns exactly **one record**, it's returned as an object +* When a query returns **multiple records**, they're returned as an array +* Set `X-Single-Record-As-Object: false` to always receive arrays +* Works with all response formats (simple, detail, syncfusion) +* Applies to both read operations and create/update returning clauses + +## CORS & OPTIONS Support + +RestHeadSpec includes 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"} +``` + +## Advanced Features + +### Base64 Encoding + +For complex header values, use base64 encoding: + +```http +GET /public/users HTTP/1.1 +X-Select-Fields-Base64: aWQsbmFtZSxlbWFpbA== +``` + +### AND/OR Logic + +Combine multiple filters with AND/OR logic: + +```http +GET /public/users HTTP/1.1 +X-FieldFilter-Status: active +X-SearchOp-Gte-Age: 18 +X-Filter-Logic: AND +``` + +### Complex Preloading + +Load nested relationships: + +```http +GET /public/users HTTP/1.1 +X-Preload: posts:id,title,comments:id,text,author:name +``` + +## Model Registration + +```go +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Email string `json:"email"` + Posts []Post `json:"posts,omitempty" gorm:"foreignKey:UserID"` +} + +// Schema.Table format +handler.Registry.RegisterModel("public.users", &User{}) +``` + +## Complete Example + +```go +package main + +import ( + "log" + "net/http" + + "github.com/bitechdev/ResolveSpec/pkg/restheadspec" + "github.com/gorilla/mux" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Name string `json:"name"` + Email string `json:"email"` + Status string `json:"status"` +} + +func main() { + // Connect to database + db, err := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + + // Create handler + handler := restheadspec.NewHandlerWithGORM(db) + + // Register models + handler.Registry.RegisterModel("public.users", &User{}) + + // Add hooks + handler.Hooks.Register(restheadspec.BeforeRead, func(ctx *restheadspec.HookContext) error { + log.Printf("Reading %s", ctx.Entity) + return nil + }) + + // Setup routes + router := mux.NewRouter() + restheadspec.SetupMuxRoutes(router, handler, nil) + + // Start server + log.Println("Server starting on :8080") + log.Fatal(http.ListenAndServe(":8080", router)) +} +``` + +## Testing + +RestHeadSpec is designed for testability: + +```go +import ( + "net/http/httptest" + "testing" +) + +func TestUserRead(t *testing.T) { + handler := restheadspec.NewHandlerWithGORM(testDB) + handler.Registry.RegisterModel("public.users", &User{}) + + req := httptest.NewRequest("GET", "/public/users", nil) + req.Header.Set("X-Select-Fields", "id,name") + req.Header.Set("X-Limit", "10") + + rec := httptest.NewRecorder() + // Test your handler... +} +``` + +## See Also + +* [HEADERS.md](HEADERS.md) - Complete header reference +* [Main README](../../README.md) - ResolveSpec overview +* [ResolveSpec Package](../resolvespec/README.md) - Body-based API +* [StaticWeb Package](../server/staticweb/README.md) - Static file server + +## License + +This package is part of ResolveSpec and is licensed under the MIT License. diff --git a/pkg/server/staticweb/policies/mime.go b/pkg/server/staticweb/policies/mime.go index 71e82d1..2eeb92d 100644 --- a/pkg/server/staticweb/policies/mime.go +++ b/pkg/server/staticweb/policies/mime.go @@ -156,7 +156,7 @@ func NewDefaultMIMEResolver() *DefaultMIMEResolver { resolver.RegisterMIMEType(".stl", "model/stl") // Other common web assets - resolver.RegisterMIMEType(".map", "application/json") // Source maps + resolver.RegisterMIMEType(".map", "application/json") // Source maps resolver.RegisterMIMEType(".swf", "application/x-shockwave-flash") resolver.RegisterMIMEType(".apk", "application/vnd.android.package-archive") resolver.RegisterMIMEType(".dmg", "application/x-apple-diskimage") diff --git a/pkg/server/zipfs/zipfs.go b/pkg/server/zipfs/zipfs.go index c547a1a..5c0511d 100644 --- a/pkg/server/zipfs/zipfs.go +++ b/pkg/server/zipfs/zipfs.go @@ -37,7 +37,7 @@ type ZipFile struct { func (f *ZipFile) Stat() (fs.FileInfo, error) { if f.File != nil { - return f.File.FileInfo(), nil + return f.FileInfo(), nil } return nil, fmt.Errorf("No file") } @@ -52,7 +52,7 @@ func (f *ZipFile) Close() error { func (f *ZipFile) Read(b []byte) (int, error) { if f.rc == nil { var err error - f.rc, err = f.File.Open() + f.rc, err = f.Open() if err != nil { return 0, err } @@ -83,7 +83,7 @@ func (f *ZipFile) Seek(offset int64, whence int) (int64, error) { } f.offset += offset case io.SeekEnd: - size := int64(f.File.UncompressedSize64) + size := int64(f.UncompressedSize64) if size+offset < 0 { return 0, &fs.PathError{Op: "seek", Path: f.Name, Err: fmt.Errorf("negative position")} }