fix: lint issues and docs

This commit is contained in:
Hein
2025-12-30 17:44:57 +02:00
parent 28fd88fff1
commit 9209193157
5 changed files with 1342 additions and 803 deletions

989
README.md

File diff suppressed because it is too large Load Diff

703
pkg/resolvespec/README.md Normal file
View File

@@ -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
```
### With Preloading
```http
POST /core/users HTTP/1.1
Content-Type: application/json
```
## 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.
```
## 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.

445
pkg/restheadspec/README.md Normal file
View File

@@ -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: <cursor_token>
```
**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.

View File

@@ -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")

View File

@@ -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")}
}