ResolveSpec/pkg/websocketspec/websocketspec.go
2025-12-12 16:14:47 +02:00

332 lines
8.5 KiB
Go

// Package websocketspec provides a WebSocket-based API specification for real-time
// CRUD operations with bidirectional communication and subscription support.
//
// # Key Features
//
// - Real-time bidirectional communication over WebSocket
// - CRUD operations (Create, Read, Update, Delete)
// - Real-time subscriptions with filtering
// - Lifecycle hooks for all operations
// - Database-agnostic: Works with GORM and Bun ORM through adapters
// - Automatic change notifications to subscribers
// - Connection and subscription management
//
// # Message Protocol
//
// WebSocketSpec uses JSON messages for communication:
//
// {
// "id": "unique-message-id",
// "type": "request|response|notification|subscription",
// "operation": "read|create|update|delete|subscribe|unsubscribe",
// "schema": "public",
// "entity": "users",
// "data": {...},
// "options": {
// "filters": [...],
// "columns": [...],
// "preload": [...],
// "sort": [...],
// "limit": 10
// }
// }
//
// # Usage Example
//
// // Create handler with GORM
// handler := websocketspec.NewHandlerWithGORM(db)
//
// // Register models
// handler.Registry.RegisterModel("public.users", &User{})
//
// // Setup WebSocket endpoint
// http.HandleFunc("/ws", handler.HandleWebSocket)
//
// // Start server
// http.ListenAndServe(":8080", nil)
//
// # Client Example
//
// // Connect to WebSocket
// ws := new WebSocket("ws://localhost:8080/ws")
//
// // Send read request
// ws.send(JSON.stringify({
// id: "msg-1",
// type: "request",
// operation: "read",
// entity: "users",
// options: {
// filters: [{column: "status", operator: "eq", value: "active"}],
// limit: 10
// }
// }))
//
// // Subscribe to changes
// ws.send(JSON.stringify({
// id: "msg-2",
// type: "subscription",
// operation: "subscribe",
// entity: "users",
// options: {
// filters: [{column: "status", operator: "eq", value: "active"}]
// }
// }))
package websocketspec
import (
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
"github.com/uptrace/bun"
"gorm.io/gorm"
)
// NewHandlerWithGORM creates a new Handler with GORM adapter
func NewHandlerWithGORM(db *gorm.DB) *Handler {
gormAdapter := database.NewGormAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(gormAdapter, registry)
}
// NewHandlerWithBun creates a new Handler with Bun adapter
func NewHandlerWithBun(db *bun.DB) *Handler {
bunAdapter := database.NewBunAdapter(db)
registry := modelregistry.NewModelRegistry()
return NewHandler(bunAdapter, registry)
}
// NewHandlerWithDatabase creates a new Handler with a custom database adapter
func NewHandlerWithDatabase(db common.Database, registry common.ModelRegistry) *Handler {
return NewHandler(db, registry)
}
// Example usage functions for documentation:
// ExampleWithGORM shows how to use WebSocketSpec with GORM
func ExampleWithGORM(db *gorm.DB) {
// Create handler using GORM
handler := NewHandlerWithGORM(db)
// Register models
handler.Registry().RegisterModel("public.users", &struct{}{})
// Register hooks (optional)
handler.Hooks().RegisterBefore(OperationRead, func(ctx *HookContext) error {
// Add custom logic before read operations
return nil
})
// Setup WebSocket endpoint
// http.HandleFunc("/ws", handler.HandleWebSocket)
// Start server
// http.ListenAndServe(":8080", nil)
}
// ExampleWithBun shows how to use WebSocketSpec with Bun ORM
func ExampleWithBun(bunDB *bun.DB) {
// Create handler using Bun
handler := NewHandlerWithBun(bunDB)
// Register models
handler.Registry().RegisterModel("public.users", &struct{}{})
// Setup WebSocket endpoint
// http.HandleFunc("/ws", handler.HandleWebSocket)
}
// ExampleWithHooks shows how to use lifecycle hooks
func ExampleWithHooks(db *gorm.DB) {
handler := NewHandlerWithGORM(db)
// Register a before-read hook for authorization
handler.Hooks().RegisterBefore(OperationRead, func(ctx *HookContext) error {
// Check if user has permission to read this entity
// return fmt.Errorf("unauthorized") if not allowed
return nil
})
// Register an after-create hook for logging
handler.Hooks().RegisterAfter(OperationCreate, func(ctx *HookContext) error {
// Log the created record
// logger.Info("Created record: %v", ctx.Result)
return nil
})
// Register a before-subscribe hook to limit subscriptions
handler.Hooks().Register(BeforeSubscribe, func(ctx *HookContext) error {
// Limit number of subscriptions per connection
// if len(ctx.Connection.subscriptions) >= 10 {
// return fmt.Errorf("maximum subscriptions reached")
// }
return nil
})
}
// ExampleWithSubscriptions shows subscription usage
func ExampleWithSubscriptions() {
// Client-side JavaScript example:
/*
const ws = new WebSocket("ws://localhost:8080/ws");
// Subscribe to user changes
ws.send(JSON.stringify({
id: "sub-1",
type: "subscription",
operation: "subscribe",
schema: "public",
entity: "users",
options: {
filters: [
{column: "status", operator: "eq", value: "active"}
]
}
}));
// Handle notifications
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "notification") {
console.log("User changed:", msg.data);
console.log("Operation:", msg.operation); // create, update, or delete
}
};
// Unsubscribe
ws.send(JSON.stringify({
id: "unsub-1",
type: "subscription",
operation: "unsubscribe",
subscription_id: "sub-abc123"
}));
*/
}
// ExampleCRUDOperations shows basic CRUD operations
func ExampleCRUDOperations() {
// Client-side JavaScript example:
/*
const ws = new WebSocket("ws://localhost:8080/ws");
// CREATE - Create a new user
ws.send(JSON.stringify({
id: "create-1",
type: "request",
operation: "create",
schema: "public",
entity: "users",
data: {
name: "John Doe",
email: "john@example.com",
status: "active"
}
}));
// READ - Get all active users
ws.send(JSON.stringify({
id: "read-1",
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
}
}));
// READ BY ID - Get a specific user
ws.send(JSON.stringify({
id: "read-2",
type: "request",
operation: "read",
schema: "public",
entity: "users",
record_id: "123"
}));
// UPDATE - Update a user
ws.send(JSON.stringify({
id: "update-1",
type: "request",
operation: "update",
schema: "public",
entity: "users",
record_id: "123",
data: {
name: "John Updated",
email: "john.updated@example.com"
}
}));
// DELETE - Delete a user
ws.send(JSON.stringify({
id: "delete-1",
type: "request",
operation: "delete",
schema: "public",
entity: "users",
record_id: "123"
}));
// Handle responses
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.type === "response") {
if (response.success) {
console.log("Operation successful:", response.data);
} else {
console.error("Operation failed:", response.error);
}
}
};
*/
}
// ExampleAuthentication shows how to implement authentication
func ExampleAuthentication() {
// Server-side example with authentication hook:
/*
handler := NewHandlerWithGORM(db)
// Register before-connect hook for authentication
handler.Hooks().Register(BeforeConnect, func(ctx *HookContext) error {
// Extract token from query params or headers
r := ctx.Connection.ws.UnderlyingConn().RemoteAddr()
// Validate token
// 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
})
// Use connection metadata in other hooks
handler.Hooks().RegisterBefore(OperationRead, func(ctx *HookContext) error {
// Get user from connection metadata
// userID, _ := ctx.Connection.GetMetadata("user_id")
// Add filter to only show user's own records
// if ctx.Entity == "orders" {
// ctx.Options.Filters = append(ctx.Options.Filters, common.FilterOption{
// Column: "user_id",
// Operator: "eq",
// Value: userID,
// })
// }
return nil
})
*/
}