Adds a BeforeOp HookType across resolvespec, restheadspec, websocketspec, mqttspec, and funcspec that fires before every SQL operation (read, create, update, delete, scan/query) via a new ExecuteBeforeOp helper. Also closes a row-level-security gap: resolvespec registered a BeforeScan hook for ApplyRowSecurity but never fired it, and websocketspec/mqttspec had no BeforeScan hook point at all, so row security was never applied to their queries. BeforeScan now fires right before the actual scan in all three, with the (possibly hook-modified) query used for execution.
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
- Installation
- Quick Start
- Message Protocol
- CRUD Operations
- Subscriptions
- Lifecycle Hooks
- Client Examples
- Authentication
- Error Handling
- 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
go get github.com/bitechdev/ResolveSpec
Quick Start
Server Setup
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)
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:
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:
{
"id": "msg-1",
"type": "request",
"operation": "create",
"schema": "public",
"entity": "users",
"data": {
"name": "John Doe",
"email": "john@example.com",
"status": "active"
}
}
Response:
{
"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:
{
"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:
{
"id": "msg-3",
"type": "request",
"operation": "read",
"schema": "public",
"entity": "users",
"record_id": "123"
}
Response:
{
"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
{
"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
{
"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
{
"id": "sub-1",
"type": "subscription",
"operation": "subscribe",
"schema": "public",
"entity": "users",
"options": {
"filters": [
{"column": "status", "operator": "eq", "value": "active"}
]
}
}
Response:
{
"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:
{
"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 createdupdate- Record updateddelete- Record deleted
Unsubscribe
{
"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
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
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
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:
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:
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:
{
"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 invalidmodel_not_found- Entity not registeredinvalid_model- Model validation failedread_error- Read operation failedcreate_error- Create operation failedupdate_error- Update operation faileddelete_error- Delete operation failedhook_error- Hook execution failedunauthorized- Authentication/authorization failed
Best Practices
- Always Use Message IDs: Correlate requests with responses using unique IDs
- Handle Reconnections: Implement automatic reconnection logic on the client
- Validate Data: Use before-hooks to validate data before operations
- Limit Subscriptions: Implement limits on subscriptions per connection
- Use Filters: Apply filters to subscriptions to reduce unnecessary notifications
- Implement Authentication: Always validate users before processing operations
- Handle Errors Gracefully: Display user-friendly error messages
- Clean Up: Unsubscribe when components unmount or disconnect
- Rate Limiting: Implement rate limiting to prevent abuse
- 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