mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-14 01:20:36 +00:00
727 lines
19 KiB
Markdown
727 lines
19 KiB
Markdown
# 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
|
|
|
|
- **BeforeRead** / **AfterRead**
|
|
- **BeforeCreate** / **AfterCreate**
|
|
- **BeforeUpdate** / **AfterUpdate**
|
|
- **BeforeDelete** / **AfterDelete**
|
|
- **BeforeSubscribe** / **AfterSubscribe**
|
|
- **BeforeConnect** / **AfterConnect**
|
|
|
|
### 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
|
|
|
|
Implement authentication using hooks:
|
|
|
|
```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
|