# WebSocketSpec - Real-Time WebSocket API Framework

WebSocketSpec provides a WebSocket-based API specification for real-time, bidirectional communication with full CRUD operations, subscriptions, and lifecycle hooks.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Message Protocol](#message-protocol)
- [CRUD Operations](#crud-operations)
- [Subscriptions](#subscriptions)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Client Examples](#client-examples)
- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)

## Features

- **Real-Time Bidirectional Communication**: WebSocket-based persistent connections
- **Full CRUD Operations**: Create, Read, Update, Delete with rich query options
- **Real-Time Subscriptions**: Subscribe to entity changes with filter support
- **Automatic Notifications**: Server pushes updates to subscribed clients
- **Lifecycle Hooks**: Before/after hooks for all operations
- **Database Agnostic**: Works with GORM and Bun ORM through adapters
- **Connection Management**: Automatic connection tracking and cleanup
- **Request/Response Correlation**: Message IDs for tracking requests
- **Filter & Sort**: Advanced filtering, sorting, pagination, and preloading

## Installation

```bash
go get github.com/bitechdev/ResolveSpec
```

## Quick Start

### Server Setup

```go
package main

import (
    "net/http"
    "github.com/bitechdev/ResolveSpec/pkg/websocketspec"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func main() {
    // Connect to database
    db, _ := gorm.Open(postgres.Open("your-connection-string"), &gorm.Config{})

    // Create WebSocket handler
    handler := websocketspec.NewHandlerWithGORM(db)

    // Register models
    handler.Registry.RegisterModel("public.users", &User{})
    handler.Registry.RegisterModel("public.posts", &Post{})

    // Setup WebSocket endpoint
    http.HandleFunc("/ws", handler.HandleWebSocket)

    // Start server
    http.ListenAndServe(":8080", nil)
}

type User struct {
    ID     uint   `json:"id" gorm:"primaryKey"`
    Name   string `json:"name"`
    Email  string `json:"email"`
    Status string `json:"status"`
}

type Post struct {
    ID      uint   `json:"id" gorm:"primaryKey"`
    Title   string `json:"title"`
    Content string `json:"content"`
    UserID  uint   `json:"user_id"`
}
```

### Client Setup (JavaScript)

```javascript
const ws = new WebSocket("ws://localhost:8080/ws");

ws.onopen = () => {
    console.log("Connected to WebSocket");
};

ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    console.log("Received:", message);
};

ws.onerror = (error) => {
    console.error("WebSocket error:", error);
};
```

## Message Protocol

All messages are JSON-encoded with the following structure:

```typescript
interface Message {
    id: string;                  // Unique message ID for correlation
    type: "request" | "response" | "notification" | "subscription";
    operation?: "read" | "create" | "update" | "delete" | "subscribe" | "unsubscribe" | "meta";
    schema?: string;             // Database schema
    entity: string;              // Table/model name
    record_id?: string;          // For single-record operations
    data?: any;                  // Request/response payload
    options?: QueryOptions;      // Filters, sorting, pagination
    subscription_id?: string;    // For subscription messages
    success?: boolean;           // Response success indicator
    error?: ErrorInfo;           // Error details
    metadata?: Record<string, any>; // Additional metadata
    timestamp?: string;          // Message timestamp
}

interface QueryOptions {
    filters?: FilterOption[];
    columns?: string[];
    preload?: PreloadOption[];
    sort?: SortOption[];
    limit?: number;
    offset?: number;
}
```

## CRUD Operations

### CREATE - Create New Records

**Request:**
```json
{
    "id": "msg-1",
    "type": "request",
    "operation": "create",
    "schema": "public",
    "entity": "users",
    "data": {
        "name": "John Doe",
        "email": "john@example.com",
        "status": "active"
    }
}
```

**Response:**
```json
{
    "id": "msg-1",
    "type": "response",
    "success": true,
    "data": {
        "id": 123,
        "name": "John Doe",
        "email": "john@example.com",
        "status": "active"
    },
    "timestamp": "2025-12-12T10:30:00Z"
}
```

### READ - Query Records

**Read Multiple Records:**
```json
{
    "id": "msg-2",
    "type": "request",
    "operation": "read",
    "schema": "public",
    "entity": "users",
    "options": {
        "filters": [
            {"column": "status", "operator": "eq", "value": "active"}
        ],
        "columns": ["id", "name", "email"],
        "sort": [
            {"column": "name", "direction": "asc"}
        ],
        "limit": 10,
        "offset": 0
    }
}
```

**Read Single Record:**
```json
{
    "id": "msg-3",
    "type": "request",
    "operation": "read",
    "schema": "public",
    "entity": "users",
    "record_id": "123"
}
```

**Response:**
```json
{
    "id": "msg-2",
    "type": "response",
    "success": true,
    "data": [
        {"id": 1, "name": "Alice", "email": "alice@example.com"},
        {"id": 2, "name": "Bob", "email": "bob@example.com"}
    ],
    "metadata": {
        "total": 50,
        "count": 2
    },
    "timestamp": "2025-12-12T10:30:00Z"
}
```

### UPDATE - Update Records

```json
{
    "id": "msg-4",
    "type": "request",
    "operation": "update",
    "schema": "public",
    "entity": "users",
    "record_id": "123",
    "data": {
        "name": "John Updated",
        "email": "john.updated@example.com"
    }
}
```

### DELETE - Delete Records

```json
{
    "id": "msg-5",
    "type": "request",
    "operation": "delete",
    "schema": "public",
    "entity": "users",
    "record_id": "123"
}
```

## Subscriptions

Subscriptions allow clients to receive real-time notifications when entities change.

### Subscribe to Changes

```json
{
    "id": "sub-1",
    "type": "subscription",
    "operation": "subscribe",
    "schema": "public",
    "entity": "users",
    "options": {
        "filters": [
            {"column": "status", "operator": "eq", "value": "active"}
        ]
    }
}
```

**Response:**
```json
{
    "id": "sub-1",
    "type": "response",
    "success": true,
    "data": {
        "subscription_id": "sub-abc123",
        "schema": "public",
        "entity": "users"
    },
    "timestamp": "2025-12-12T10:30:00Z"
}
```

### Receive Notifications

When a subscribed entity changes, clients automatically receive notifications:

```json
{
    "type": "notification",
    "operation": "create",
    "subscription_id": "sub-abc123",
    "schema": "public",
    "entity": "users",
    "data": {
        "id": 124,
        "name": "Jane Smith",
        "email": "jane@example.com",
        "status": "active"
    },
    "timestamp": "2025-12-12T10:35:00Z"
}
```

**Notification Operations:**
- `create` - New record created
- `update` - Record updated
- `delete` - Record deleted

### Unsubscribe

```json
{
    "id": "unsub-1",
    "type": "subscription",
    "operation": "unsubscribe",
    "subscription_id": "sub-abc123"
}
```

## Lifecycle Hooks

Hooks allow you to intercept and modify operations at various points in the lifecycle.

### Available Hook Types

- **BeforeHandle** — fires after model resolution, before operation dispatch (auth checks)
- **BeforeRead** / **AfterRead**
- **BeforeCreate** / **AfterCreate**
- **BeforeUpdate** / **AfterUpdate**
- **BeforeDelete** / **AfterDelete**
- **BeforeSubscribe** / **AfterSubscribe**
- **BeforeConnect** / **AfterConnect**

`HookContext` includes `Operation string` (`"read"`, `"create"`, `"update"`, `"delete"`) and `Abort bool`, `AbortMessage string`, `AbortCode int` for abort signaling.

### Hook Example

```go
handler := websocketspec.NewHandlerWithGORM(db)

// Authorization hook
handler.Hooks().RegisterBefore(websocketspec.OperationRead, func(ctx *websocketspec.HookContext) error {
    // Check permissions
    userID, _ := ctx.Connection.GetMetadata("user_id")
    if userID == nil {
        return fmt.Errorf("unauthorized: user not authenticated")
    }

    // Add filter to only show user's own records
    if ctx.Entity == "posts" {
        ctx.Options.Filters = append(ctx.Options.Filters, common.FilterOption{
            Column:   "user_id",
            Operator: "eq",
            Value:    userID,
        })
    }

    return nil
})

// Logging hook
handler.Hooks().RegisterAfter(websocketspec.OperationCreate, func(ctx *websocketspec.HookContext) error {
    log.Printf("Created %s in %s.%s", ctx.Result, ctx.Schema, ctx.Entity)
    return nil
})

// Validation hook
handler.Hooks().RegisterBefore(websocketspec.OperationCreate, func(ctx *websocketspec.HookContext) error {
    // Validate data before creation
    if data, ok := ctx.Data.(map[string]interface{}); ok {
        if email, exists := data["email"]; !exists || email == "" {
            return fmt.Errorf("email is required")
        }
    }
    return nil
})
```

## Client Examples

### JavaScript/TypeScript Client

```typescript
class WebSocketClient {
    private ws: WebSocket;
    private messageHandlers: Map<string, (data: any) => void> = new Map();
    private subscriptions: Map<string, (data: any) => void> = new Map();

    constructor(url: string) {
        this.ws = new WebSocket(url);
        this.ws.onmessage = (event) => this.handleMessage(event);
    }

    // Send request and wait for response
    async request(operation: string, entity: string, options?: any): Promise<any> {
        const id = this.generateId();

        return new Promise((resolve, reject) => {
            this.messageHandlers.set(id, (data) => {
                if (data.success) {
                    resolve(data.data);
                } else {
                    reject(data.error);
                }
            });

            this.ws.send(JSON.stringify({
                id,
                type: "request",
                operation,
                entity,
                ...options
            }));
        });
    }

    // Subscribe to entity changes
    async subscribe(entity: string, filters?: any[], callback?: (data: any) => void): Promise<string> {
        const id = this.generateId();

        return new Promise((resolve, reject) => {
            this.messageHandlers.set(id, (data) => {
                if (data.success) {
                    const subId = data.data.subscription_id;
                    if (callback) {
                        this.subscriptions.set(subId, callback);
                    }
                    resolve(subId);
                } else {
                    reject(data.error);
                }
            });

            this.ws.send(JSON.stringify({
                id,
                type: "subscription",
                operation: "subscribe",
                entity,
                options: { filters }
            }));
        });
    }

    private handleMessage(event: MessageEvent) {
        const message = JSON.parse(event.data);

        if (message.type === "response") {
            const handler = this.messageHandlers.get(message.id);
            if (handler) {
                handler(message);
                this.messageHandlers.delete(message.id);
            }
        } else if (message.type === "notification") {
            const callback = this.subscriptions.get(message.subscription_id);
            if (callback) {
                callback(message);
            }
        }
    }

    private generateId(): string {
        return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    }
}

// Usage
const client = new WebSocketClient("ws://localhost:8080/ws");

// Read users
const users = await client.request("read", "users", {
    options: {
        filters: [{ column: "status", operator: "eq", value: "active" }],
        limit: 10
    }
});

// Subscribe to user changes
await client.subscribe("users",
    [{ column: "status", operator: "eq", value: "active" }],
    (notification) => {
        console.log("User changed:", notification.operation, notification.data);
    }
);

// Create user
const newUser = await client.request("create", "users", {
    data: {
        name: "Alice",
        email: "alice@example.com",
        status: "active"
    }
});
```

### Python Client Example

```python
import asyncio
import websockets
import json
import uuid

class WebSocketClient:
    def __init__(self, url):
        self.url = url
        self.ws = None
        self.handlers = {}
        self.subscriptions = {}

    async def connect(self):
        self.ws = await websockets.connect(self.url)
        asyncio.create_task(self.listen())

    async def listen(self):
        async for message in self.ws:
            data = json.loads(message)

            if data["type"] == "response":
                handler = self.handlers.get(data["id"])
                if handler:
                    handler(data)
                    del self.handlers[data["id"]]

            elif data["type"] == "notification":
                callback = self.subscriptions.get(data["subscription_id"])
                if callback:
                    callback(data)

    async def request(self, operation, entity, **kwargs):
        msg_id = str(uuid.uuid4())
        future = asyncio.Future()

        self.handlers[msg_id] = lambda data: future.set_result(data)

        await self.ws.send(json.dumps({
            "id": msg_id,
            "type": "request",
            "operation": operation,
            "entity": entity,
            **kwargs
        }))

        result = await future
        if result["success"]:
            return result["data"]
        else:
            raise Exception(result["error"]["message"])

    async def subscribe(self, entity, callback, filters=None):
        msg_id = str(uuid.uuid4())
        future = asyncio.Future()

        self.handlers[msg_id] = lambda data: future.set_result(data)

        await self.ws.send(json.dumps({
            "id": msg_id,
            "type": "subscription",
            "operation": "subscribe",
            "entity": entity,
            "options": {"filters": filters} if filters else {}
        }))

        result = await future
        if result["success"]:
            sub_id = result["data"]["subscription_id"]
            self.subscriptions[sub_id] = callback
            return sub_id
        else:
            raise Exception(result["error"]["message"])

# Usage
async def main():
    client = WebSocketClient("ws://localhost:8080/ws")
    await client.connect()

    # Read users
    users = await client.request("read", "users",
        options={
            "filters": [{"column": "status", "operator": "eq", "value": "active"}],
            "limit": 10
        }
    )
    print("Users:", users)

    # Subscribe to changes
    def on_user_change(notification):
        print(f"User {notification['operation']}: {notification['data']}")

    await client.subscribe("users", on_user_change,
        filters=[{"column": "status", "operator": "eq", "value": "active"}]
    )

asyncio.run(main())
```

## Authentication

Use `RegisterSecurityHooks` for integrated auth with model-rule support:

```go
import "github.com/bitechdev/ResolveSpec/pkg/security"

provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
securityList := security.NewSecurityList(provider)
websocketspec.RegisterSecurityHooks(handler, securityList)
// Registers BeforeHandle (model auth), BeforeRead (load rules),
// AfterRead (column security + audit), BeforeUpdate, BeforeDelete
```

Or implement custom authentication using hooks directly:

```go
handler := websocketspec.NewHandlerWithGORM(db)

// Authentication on connection
handler.Hooks().Register(websocketspec.BeforeConnect, func(ctx *websocketspec.HookContext) error {
    // Extract token from query params or headers
    r := ctx.Connection.ws.UnderlyingConn().RemoteAddr()

    // Validate token (implement your auth logic)
    token := extractToken(r)
    user, err := validateToken(token)
    if err != nil {
        return fmt.Errorf("authentication failed: %w", err)
    }

    // Store user info in connection metadata
    ctx.Connection.SetMetadata("user", user)
    ctx.Connection.SetMetadata("user_id", user.ID)

    return nil
})

// Check permissions for each operation
handler.Hooks().RegisterBefore(websocketspec.OperationRead, func(ctx *websocketspec.HookContext) error {
    userID, ok := ctx.Connection.GetMetadata("user_id")
    if !ok {
        return fmt.Errorf("unauthorized")
    }

    // Add user-specific filters
    if ctx.Entity == "orders" {
        ctx.Options.Filters = append(ctx.Options.Filters, common.FilterOption{
            Column:   "user_id",
            Operator: "eq",
            Value:    userID,
        })
    }

    return nil
})
```

## Error Handling

Errors are returned in a consistent format:

```json
{
    "id": "msg-1",
    "type": "response",
    "success": false,
    "error": {
        "code": "validation_error",
        "message": "Email is required",
        "details": {
            "field": "email"
        }
    },
    "timestamp": "2025-12-12T10:30:00Z"
}
```

**Common Error Codes:**
- `invalid_message` - Message format is invalid
- `model_not_found` - Entity not registered
- `invalid_model` - Model validation failed
- `read_error` - Read operation failed
- `create_error` - Create operation failed
- `update_error` - Update operation failed
- `delete_error` - Delete operation failed
- `hook_error` - Hook execution failed
- `unauthorized` - Authentication/authorization failed

## Best Practices

1. **Always Use Message IDs**: Correlate requests with responses using unique IDs
2. **Handle Reconnections**: Implement automatic reconnection logic on the client
3. **Validate Data**: Use before-hooks to validate data before operations
4. **Limit Subscriptions**: Implement limits on subscriptions per connection
5. **Use Filters**: Apply filters to subscriptions to reduce unnecessary notifications
6. **Implement Authentication**: Always validate users before processing operations
7. **Handle Errors Gracefully**: Display user-friendly error messages
8. **Clean Up**: Unsubscribe when components unmount or disconnect
9. **Rate Limiting**: Implement rate limiting to prevent abuse
10. **Monitor Connections**: Track active connections and subscriptions

## Filter Operators

Supported filter operators:

- `eq` - Equal (=)
- `neq` - Not Equal (!=)
- `gt` - Greater Than (>)
- `gte` - Greater Than or Equal (>=)
- `lt` - Less Than (<)
- `lte` - Less Than or Equal (<=)
- `like` - LIKE (case-sensitive)
- `ilike` - ILIKE (case-insensitive)
- `in` - IN (array of values)

## Performance Considerations

- **Connection Pooling**: WebSocket connections are reused, reducing overhead
- **Subscription Filtering**: Only matching updates are sent to clients
- **Efficient Queries**: Uses database adapters for optimized queries
- **Message Batching**: Multiple messages can be sent in one write
- **Keepalive**: Automatic ping/pong for connection health

## Comparison with Other Specs

| Feature | WebSocketSpec | RestHeadSpec | ResolveSpec |
|---------|--------------|--------------|-------------|
| Protocol | WebSocket | HTTP/REST | HTTP/REST |
| Real-time | ✅ Yes | ❌ No | ❌ No |
| Subscriptions | ✅ Yes | ❌ No | ❌ No |
| Bidirectional | ✅ Yes | ❌ No | ❌ No |
| Query Options | In Message | In Headers | In Body |
| Overhead | Low | Medium | Medium |
| Use Case | Real-time apps | Traditional APIs | Body-based APIs |

## License

MIT License - See LICENSE file for details
