feat: add zero-configuration Brainy service template with augmentation-first architecture

- Implement WebSocket augmentation for real-time communication
- Implement WebRTC augmentation for peer-to-peer connections
- Implement HTTP augmentation as minimal REST fallback
- Add auto-discovery augmentation for data pattern analysis
- Add adaptive storage augmentation for intelligent resource management
- Add environment adapter augmentation for universal compatibility
- Template auto-detects environment (browser, Node.js, serverless, containers)
- Intelligent transport selection (WebRTC → WebSocket → HTTP)
- Automatic storage optimization (memory → filesystem → S3)
- Zero configuration required - just npm start
- Includes intelligent verb scoring by default
- Works in any environment without configuration
- Full documentation and examples included
This commit is contained in:
David Snelling 2025-08-06 18:17:32 -07:00
parent 880b8f74e3
commit ee003a9473
30 changed files with 3548 additions and 0 deletions

View file

@ -0,0 +1,136 @@
import { logger } from '../utils/logger.js'
import { ApiError } from '../utils/errors.js'
export const errorHandler = (error, req, res, next) => {
// Default error response
let statusCode = 500
let message = 'Internal Server Error'
let details = null
// Handle known API errors
if (error instanceof ApiError) {
statusCode = error.statusCode
message = error.message
// Add additional details for validation errors
if (error.field) {
details = { field: error.field }
}
if (error.resource) {
details = {
resource: error.resource,
resourceId: error.resourceId
}
}
}
// Handle Brainy-specific errors
else if (error.name === 'BrainyError') {
statusCode = 400
message = error.message
details = { type: 'brainy_error' }
}
// Handle validation errors from other sources
else if (error.name === 'ValidationError' || error.name === 'CastError') {
statusCode = 400
message = error.message
details = { type: 'validation_error' }
}
// Handle JSON parsing errors
else if (error instanceof SyntaxError && error.status === 400 && 'body' in error) {
statusCode = 400
message = 'Invalid JSON format'
details = { type: 'json_error' }
}
// Handle timeout errors
else if (error.code === 'ETIMEDOUT' || error.message.includes('timeout')) {
statusCode = 504
message = 'Request timeout'
details = { type: 'timeout_error' }
}
// Handle rate limiting
else if (error.status === 429) {
statusCode = 429
message = 'Too Many Requests'
details = { type: 'rate_limit_error' }
}
// Log the error
const logLevel = statusCode >= 500 ? 'error' : 'warn'
const logMessage = `${req.method} ${req.originalUrl} - ${statusCode} ${message}`
const logMeta = {
statusCode,
method: req.method,
url: req.originalUrl,
userAgent: req.get('User-Agent'),
ip: req.ip,
stack: error.stack
}
logger[logLevel](logMessage, logMeta)
// Prepare error response
const errorResponse = {
success: false,
error: {
message,
statusCode,
timestamp: new Date().toISOString(),
path: req.originalUrl,
method: req.method
}
}
// Add details if available
if (details) {
errorResponse.error.details = details
}
// Add stack trace in development
if (process.env.NODE_ENV === 'development') {
errorResponse.error.stack = error.stack
}
// Add request ID if available
if (req.id) {
errorResponse.error.requestId = req.id
}
res.status(statusCode).json(errorResponse)
}
// 404 handler for unmatched routes
export const notFoundHandler = (req, res) => {
const message = `Route ${req.method} ${req.originalUrl} not found`
logger.warn('Route not found', {
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent')
})
res.status(404).json({
success: false,
error: {
message,
statusCode: 404,
timestamp: new Date().toISOString(),
path: req.originalUrl,
method: req.method
}
})
}
// Async error wrapper
export const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next)
}
}
export default {
errorHandler,
notFoundHandler,
asyncHandler
}

View file

@ -0,0 +1,177 @@
import { ValidationError } from '../utils/errors.js'
// Validate required fields
export const validateRequired = (fields) => {
return (req, res, next) => {
const missingFields = []
fields.forEach(field => {
if (req.body[field] === undefined || req.body[field] === null || req.body[field] === '') {
missingFields.push(field)
}
})
if (missingFields.length > 0) {
throw new ValidationError(`Missing required fields: ${missingFields.join(', ')}`)
}
next()
}
}
// Validate entity creation
export const validateEntity = (req, res, next) => {
const { data } = req.body
if (!data) {
throw new ValidationError('Entity data is required')
}
if (typeof data !== 'object' && typeof data !== 'string') {
throw new ValidationError('Entity data must be an object or string')
}
next()
}
// Validate relationship creation
export const validateRelationship = (req, res, next) => {
const { sourceId, targetId, type, weight } = req.body
if (!sourceId || !targetId || !type) {
throw new ValidationError('sourceId, targetId, and type are required')
}
if (typeof sourceId !== 'string' || typeof targetId !== 'string' || typeof type !== 'string') {
throw new ValidationError('sourceId, targetId, and type must be strings')
}
if (sourceId === targetId) {
throw new ValidationError('sourceId and targetId cannot be the same')
}
if (weight !== undefined) {
if (typeof weight !== 'number' || weight < 0 || weight > 1) {
throw new ValidationError('Weight must be a number between 0 and 1')
}
}
next()
}
// Validate search query
export const validateSearch = (req, res, next) => {
const { query, limit, threshold } = req.body
if (!query) {
throw new ValidationError('Search query is required')
}
if (typeof query !== 'string' || query.trim().length === 0) {
throw new ValidationError('Search query must be a non-empty string')
}
if (limit !== undefined) {
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new ValidationError('Limit must be an integer between 1 and 100')
}
}
if (threshold !== undefined) {
if (typeof threshold !== 'number' || threshold < 0 || threshold > 1) {
throw new ValidationError('Threshold must be a number between 0 and 1')
}
}
next()
}
// Validate feedback data
export const validateFeedback = (req, res, next) => {
const { weight, confidence, type } = req.body
if (weight === undefined) {
throw new ValidationError('Weight is required for feedback')
}
if (typeof weight !== 'number' || weight < 0 || weight > 1) {
throw new ValidationError('Weight must be a number between 0 and 1')
}
if (confidence !== undefined) {
if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
throw new ValidationError('Confidence must be a number between 0 and 1')
}
}
if (type !== undefined) {
const validTypes = ['correction', 'reinforcement', 'adjustment']
if (!validTypes.includes(type)) {
throw new ValidationError(`Type must be one of: ${validTypes.join(', ')}`)
}
}
next()
}
// Validate pagination parameters
export const validatePagination = (req, res, next) => {
const { page = 1, limit = 50 } = req.query
const pageNum = parseInt(page)
const limitNum = parseInt(limit)
if (!Number.isInteger(pageNum) || pageNum < 1) {
throw new ValidationError('Page must be a positive integer')
}
if (!Number.isInteger(limitNum) || limitNum < 1 || limitNum > 100) {
throw new ValidationError('Limit must be an integer between 1 and 100')
}
// Normalize values
req.query.page = pageNum
req.query.limit = limitNum
next()
}
// Validate UUID format (basic check)
export const validateId = (paramName = 'id') => {
return (req, res, next) => {
const id = req.params[paramName]
if (!id) {
throw new ValidationError(`${paramName} is required`)
}
// Basic string validation - Brainy uses various ID formats
if (typeof id !== 'string' || id.trim().length === 0) {
throw new ValidationError(`${paramName} must be a valid string`)
}
next()
}
}
// Generic validation wrapper
export const validate = (validationFn) => {
return (req, res, next) => {
try {
validationFn(req, res, next)
} catch (error) {
next(error)
}
}
}
export default {
validateRequired,
validateEntity,
validateRelationship,
validateSearch,
validateFeedback,
validatePagination,
validateId,
validate
}