From eaf44833f66bf013040363735d559bd55cf314f9 Mon Sep 17 00:00:00 2001 From: "Abdulmalik A." Date: Sat, 28 Mar 2026 21:38:34 +0100 Subject: [PATCH] Revert "Revert "Revert "Middleware Performance Benchmarks & External Plugin System""" --- ONBOARDING_FLOW_DIAGRAM.md | 0 ONBOARDING_IMPLEMENTATION_SUMMARY.md | 196 ++++++ ONBOARDING_QUICKSTART.md | 268 +++++++ middleware/README.md | 58 -- middleware/docs/PERFORMANCE.md | 84 --- middleware/docs/PLUGINS.md | 651 ------------------ middleware/docs/PLUGIN_QUICKSTART.md | 480 ------------- middleware/package.json | 8 +- middleware/scripts/benchmark.ts | 354 ---------- middleware/src/common/interfaces/index.ts | 3 - .../src/common/interfaces/plugin.errors.ts | 153 ---- .../src/common/interfaces/plugin.interface.ts | 244 ------- middleware/src/common/utils/index.ts | 5 - middleware/src/common/utils/plugin-loader.ts | 628 ----------------- .../src/common/utils/plugin-registry.ts | 370 ---------- middleware/src/index.ts | 6 - middleware/src/plugins/example.plugin.ts | 193 ------ middleware/src/security/index.ts | 5 +- .../integration/benchmark.integration.spec.ts | 42 -- .../plugin-system.integration.spec.ts | 262 ------- middleware/tsconfig.json | 2 +- 21 files changed, 468 insertions(+), 3544 deletions(-) create mode 100644 ONBOARDING_FLOW_DIAGRAM.md create mode 100644 ONBOARDING_IMPLEMENTATION_SUMMARY.md create mode 100644 ONBOARDING_QUICKSTART.md delete mode 100644 middleware/docs/PLUGINS.md delete mode 100644 middleware/docs/PLUGIN_QUICKSTART.md delete mode 100644 middleware/scripts/benchmark.ts delete mode 100644 middleware/src/common/interfaces/index.ts delete mode 100644 middleware/src/common/interfaces/plugin.errors.ts delete mode 100644 middleware/src/common/interfaces/plugin.interface.ts delete mode 100644 middleware/src/common/utils/index.ts delete mode 100644 middleware/src/common/utils/plugin-loader.ts delete mode 100644 middleware/src/common/utils/plugin-registry.ts delete mode 100644 middleware/src/plugins/example.plugin.ts delete mode 100644 middleware/tests/integration/benchmark.integration.spec.ts delete mode 100644 middleware/tests/integration/plugin-system.integration.spec.ts diff --git a/ONBOARDING_FLOW_DIAGRAM.md b/ONBOARDING_FLOW_DIAGRAM.md new file mode 100644 index 00000000..e69de29b diff --git a/ONBOARDING_IMPLEMENTATION_SUMMARY.md b/ONBOARDING_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..434ff43e --- /dev/null +++ b/ONBOARDING_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,196 @@ +# Onboarding Flow Backend Integration - Implementation Summary + +## โœ… Completed Tasks + +### 1. API Service Layer + +**File**: `frontend/lib/api/userApi.ts` + +- Created `updateUserProfile()` function for PATCH `/users/{userId}` +- Implemented comprehensive error handling with custom `UserApiError` class +- Added authentication via Bearer token from localStorage +- Network error detection with user-friendly messages +- Proper TypeScript types for request/response + +### 2. React Hook + +**File**: `frontend/hooks/useUpdateUserProfile.ts` + +- Created `useUpdateUserProfile()` custom hook +- Manages loading, error states +- Integrates with Redux auth store via `useAuth()` +- Updates user data in store after successful API call +- Provides `clearError()` for error recovery + +### 3. Enum Mapping Utility + +**File**: `frontend/lib/utils/onboardingMapper.ts` + +- Maps frontend display values to backend enum values +- Handles all 4 data types: challengeLevel, challengeTypes, referralSource, ageGroup +- Ensures data compatibility between frontend and backend + +### 4. OnboardingContext Updates + +**File**: `frontend/app/onboarding/OnboardingContext.tsx` + +- Simplified data structure to match backend requirements +- Removed nested objects (additionalInfo, availability) +- Added `resetData()` method to clear state after successful save +- Maintains state across all onboarding steps + +### 5. Additional Info Page Integration + +**File**: `frontend/app/onboarding/additional-info/page.tsx` + +- Integrated API call on final step completion +- Added loading screen with animated progress bar +- Added error screen with retry functionality +- Implements proper data mapping before API call +- Redirects to dashboard on success +- Resets onboarding context after save + +### 6. Documentation + +**File**: `frontend/docs/ONBOARDING_INTEGRATION.md` + +- Comprehensive architecture documentation +- Data flow diagrams +- Error handling guide +- Testing checklist +- Future enhancement suggestions + +## ๐ŸŽฏ Key Features Implemented + +### โœ… Single API Call + +- All onboarding data collected across 4 steps +- Single PATCH request made only on final step completion +- No intermediate API calls + +### โœ… Loading States + +- "Preparing your account..." loading screen +- Animated progress bar (0-100%) +- Smooth transitions + +### โœ… Error Handling + +- Network errors: "Unable to connect. Please check your internet connection." +- Auth errors: "Unauthorized. Please log in again." +- Validation errors: Display specific field errors from backend +- Server errors: "Something went wrong. Please try again." +- Retry functionality +- Skip option to proceed to dashboard + +### โœ… Form Validation + +- Continue buttons disabled until selection made +- Data format validation via enum mapping +- Authentication check before submission + +### โœ… Success Flow + +- Redux store updated with new user data +- Onboarding context reset +- Automatic redirect to `/dashboard` +- No re-showing of onboarding (context cleared) + +### โœ… User Experience + +- Back navigation works on all steps +- Progress bar shows completion percentage +- Clear error messages +- Retry and skip options on error +- Smooth animations and transitions + +## ๐Ÿ“‹ Acceptance Criteria Status + +| Criteria | Status | Notes | +| --------------------------------------------- | ------ | ------------------------------- | +| Onboarding data collected from all four steps | โœ… | Via OnboardingContext | +| API call made only after step 4 completion | โœ… | In additional-info page | +| Single PATCH request with all data | โœ… | updateUserProfile() | +| "Preparing account" loading state shown | โœ… | With animated progress | +| On success, redirect to /dashboard | โœ… | router.push('/dashboard') | +| On error, show message with retry | โœ… | Error screen component | +| Form validation prevents invalid data | โœ… | Enum mapping + disabled buttons | +| Loading and error states handled | โœ… | Comprehensive state management | +| User cannot skip onboarding | โœ… | No skip buttons on steps 1-3 | + +## ๐Ÿ”ง Technical Details + +### API Endpoint + +``` +PATCH /users/{userId} +Authorization: Bearer {accessToken} +Content-Type: application/json +``` + +### Request Body Structure + +```json +{ + "challengeLevel": "beginner", + "challengeTypes": ["Coding Challenges", "Logic Puzzle"], + "referralSource": "Google Search", + "ageGroup": "18-24 years old" +} +``` + +### Authentication + +- Token retrieved from localStorage ('accessToken') +- User ID from Redux auth store +- Automatic 401 handling + +### State Management + +- OnboardingContext: Temporary onboarding data +- Redux Auth Store: Persistent user data +- Context reset after successful save + +## ๐Ÿงช Testing Recommendations + +1. **Happy Path** + - Complete all 4 steps + - Verify API call with correct data + - Confirm redirect to dashboard + - Check Redux store updated + +2. **Error Scenarios** + - Network offline: Check error message + - Invalid token: Check auth error + - Server error: Check retry functionality + - Validation error: Check field errors + +3. **Navigation** + - Back button on each step + - Progress bar updates correctly + - Data persists across navigation + +4. **Edge Cases** + - User not authenticated + - Missing token + - Incomplete data + - Multiple rapid submissions + +## ๐Ÿ“ Notes + +- All TypeScript types properly defined +- No console errors or warnings +- Follows existing code patterns +- Minimal dependencies added +- Clean separation of concerns +- Comprehensive error handling +- User-friendly error messages + +## ๐Ÿš€ Next Steps (Optional Enhancements) + +1. Add onboarding completion flag to prevent re-showing +2. Implement progress persistence in localStorage +3. Add analytics tracking +4. Add skip option on earlier steps (if fields are optional) +5. Add client-side validation before submission +6. Add loading skeleton for dashboard after redirect diff --git a/ONBOARDING_QUICKSTART.md b/ONBOARDING_QUICKSTART.md new file mode 100644 index 00000000..67bb541d --- /dev/null +++ b/ONBOARDING_QUICKSTART.md @@ -0,0 +1,268 @@ +# Onboarding Integration - Quick Start Guide + +## ๐Ÿš€ What Was Built + +The onboarding flow now saves user data to the backend when users complete all 4 steps. + +## ๐Ÿ“ New Files Created + +``` +frontend/ +โ”œโ”€โ”€ lib/ +โ”‚ โ”œโ”€โ”€ api/ +โ”‚ โ”‚ โ””โ”€โ”€ userApi.ts # API service for user profile updates +โ”‚ โ””โ”€โ”€ utils/ +โ”‚ โ””โ”€โ”€ onboardingMapper.ts # Maps frontend values to backend enums +โ”œโ”€โ”€ hooks/ +โ”‚ โ””โ”€โ”€ useUpdateUserProfile.ts # React hook for profile updates +โ””โ”€โ”€ docs/ + โ””โ”€โ”€ ONBOARDING_INTEGRATION.md # Detailed documentation +``` + +## ๐Ÿ“ Modified Files + +``` +frontend/app/onboarding/ +โ”œโ”€โ”€ OnboardingContext.tsx # Simplified data structure +โ””โ”€โ”€ additional-info/page.tsx # Added API integration +``` + +## ๐Ÿ”„ How It Works + +### User Flow + +1. User selects challenge level โ†’ stored in context +2. User selects challenge types โ†’ stored in context +3. User selects referral source โ†’ stored in context +4. User selects age group โ†’ **API call triggered** +5. Loading screen shows "Preparing your account..." +6. On success โ†’ Redirect to dashboard +7. On error โ†’ Show error with retry option + +### Technical Flow + +``` +OnboardingContext (state) + โ†“ +additional-info/page.tsx (final step) + โ†“ +useUpdateUserProfile() hook + โ†“ +updateUserProfile() API call + โ†“ +PATCH /users/{userId} + โ†“ +Success: Update Redux + Redirect +Error: Show error screen +``` + +## ๐Ÿงช How to Test + +### 1. Start the Application + +```bash +# Backend +cd backend +npm run start:dev + +# Frontend +cd frontend +npm run dev +``` + +### 2. Test Happy Path + +1. Navigate to `/onboarding` +2. Complete all 4 steps +3. Verify loading screen appears +4. Verify redirect to `/dashboard` +5. Check browser DevTools Network tab for PATCH request +6. Verify user data saved in database + +### 3. Test Error Handling + +```bash +# Test network error (stop backend) +npm run stop + +# Test auth error (clear localStorage) +localStorage.removeItem('accessToken') + +# Test validation error (modify enum values) +``` + +## ๐Ÿ” Debugging + +### Check API Call + +```javascript +// Open browser console on final onboarding step +// Look for: +// - PATCH request to /users/{userId} +// - Request headers (Authorization: Bearer ...) +// - Request body (challengeLevel, challengeTypes, etc.) +// - Response status (200 = success) +``` + +### Check State + +```javascript +// In OnboardingContext +console.log("Onboarding data:", data); + +// In useUpdateUserProfile +console.log("Loading:", isLoading); +console.log("Error:", error); +``` + +### Common Issues + +**Issue**: "User not authenticated" error + +- **Fix**: Ensure user is logged in and token exists in localStorage + +**Issue**: API call returns 400 validation error + +- **Fix**: Check enum mapping in `onboardingMapper.ts` + +**Issue**: Loading screen stuck + +- **Fix**: Check network tab for failed request, verify backend is running + +**Issue**: Redirect not working + +- **Fix**: Check router.push('/dashboard') is called after success + +## ๐Ÿ“Š API Request Example + +### Request + +```http +PATCH /users/123e4567-e89b-12d3-a456-426614174000 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json + +{ + "challengeLevel": "intermediate", + "challengeTypes": ["Coding Challenges", "Logic Puzzle"], + "referralSource": "Google Search", + "ageGroup": "25-34 years old" +} +``` + +### Response (Success) + +```json +{ + "id": "123e4567-e89b-12d3-a456-426614174000", + "username": "john_doe", + "email": "john@example.com", + "challengeLevel": "intermediate", + "challengeTypes": ["Coding Challenges", "Logic Puzzle"], + "referralSource": "Google Search", + "ageGroup": "25-34 years old", + "xp": 0, + "level": 1 +} +``` + +### Response (Error) + +```json +{ + "statusCode": 400, + "message": "Validation failed", + "error": "Bad Request" +} +``` + +## ๐ŸŽจ UI States + +### Loading State + +- Animated puzzle icon (bouncing) +- Progress bar (0-100%) +- Message: "Preparing your account..." + +### Error State + +- Red error icon +- Error message (specific to error type) +- "Try Again" button +- "Skip for now" link + +### Success State + +- Automatic redirect to dashboard +- No manual confirmation needed + +## ๐Ÿ” Security + +- โœ… Authentication required (Bearer token) +- โœ… User ID from authenticated session +- โœ… Token stored securely in localStorage +- โœ… HTTPS recommended for production +- โœ… No sensitive data in URL params + +## ๐Ÿ“ˆ Monitoring + +### What to Monitor + +- API success rate +- Average response time +- Error types and frequency +- Completion rate (users who finish all steps) +- Drop-off points (which step users leave) + +### Logging + +```javascript +// Add to production +console.log("Onboarding completed:", { + userId: user.id, + timestamp: new Date().toISOString(), + data: profileData, +}); +``` + +## ๐Ÿšจ Error Messages + +| Error Type | User Message | Action | +| ---------------- | ----------------------------------------------------------- | ----------------- | +| Network | "Unable to connect. Please check your internet connection." | Retry | +| Auth (401) | "Unauthorized. Please log in again." | Redirect to login | +| Validation (400) | "Invalid data provided" | Show field errors | +| Server (500) | "Something went wrong. Please try again." | Retry | +| Unknown | "An unexpected error occurred. Please try again." | Retry | + +## โœ… Checklist Before Deployment + +- [ ] Environment variable `NEXT_PUBLIC_API_URL` set correctly +- [ ] Backend endpoint `/users/{userId}` is accessible +- [ ] Authentication middleware configured +- [ ] CORS enabled for frontend domain +- [ ] Error logging configured +- [ ] Analytics tracking added (optional) +- [ ] Load testing completed +- [ ] User acceptance testing completed + +## ๐Ÿ“ž Support + +For issues or questions: + +1. Check `frontend/docs/ONBOARDING_INTEGRATION.md` for detailed docs +2. Review `ONBOARDING_IMPLEMENTATION_SUMMARY.md` for architecture +3. Check browser console for errors +4. Check backend logs for API errors +5. Verify environment variables are set + +## ๐ŸŽฏ Success Metrics + +- โœ… All 4 onboarding steps navigate correctly +- โœ… Data persists across navigation +- โœ… API call succeeds with correct data +- โœ… Loading state shows during API call +- โœ… Success redirects to dashboard +- โœ… Errors show user-friendly messages +- โœ… Retry functionality works +- โœ… No console errors or warnings diff --git a/middleware/README.md b/middleware/README.md index 0e142014..39c04a88 100644 --- a/middleware/README.md +++ b/middleware/README.md @@ -20,48 +20,6 @@ Keeping middleware in its own workspace package makes it: - Monitoring - Validation - Common utilities -- **Plugin System** - Load custom middleware from npm packages - -## Plugin System - -The package includes an **External Plugin Loader** system that allows you to dynamically load and manage middleware plugins from npm packages. - -```typescript -import { PluginRegistry } from '@mindblock/middleware'; - -// Create and initialize registry -const registry = new PluginRegistry(); -await registry.init(); - -// Load a plugin -const plugin = await registry.load('@yourorg/plugin-example'); - -// Activate it -await registry.activate(plugin.metadata.id); - -// Use plugin middleware -const middlewares = registry.getAllMiddleware(); -app.use(middlewares['com.yourorg.plugin.example']); -``` - -**Key Features:** -- โœ… Dynamic plugin discovery and loading from npm -- โœ… Plugin lifecycle management (load, init, activate, deactivate, unload) -- โœ… Configuration validation with JSON Schema support -- โœ… Dependency resolution between plugins -- โœ… Version compatibility checking -- โœ… Plugin registry and search capabilities -- โœ… Comprehensive error handling - -See [PLUGINS.md](docs/PLUGINS.md) for complete documentation on creating and using plugins. - -### Getting Started with Plugins - -To quickly start developing a plugin: - -1. Read the [Plugin Quick Start Guide](docs/PLUGIN_QUICKSTART.md) -2. Check out the [Example Plugin](src/plugins/example.plugin.ts) -3. Review plugin [API Reference](src/common/interfaces/plugin.interface.ts) ## Installation @@ -85,22 +43,6 @@ You can also import by category (once the exports exist): import { /* future exports */ } from '@mindblock/middleware/auth'; ``` -## Performance Benchmarking - -This package includes automated performance benchmarks to measure the latency -overhead of each middleware component individually. - -```bash -# Run performance benchmarks -npm run benchmark - -# Run with CI-friendly output -npm run benchmark:ci -``` - -See [PERFORMANCE.md](docs/PERFORMANCE.md) for detailed benchmarking documentation -and optimization techniques. - ## Quick Start Example placeholder usage (actual middleware implementations will be added in later issues): diff --git a/middleware/docs/PERFORMANCE.md b/middleware/docs/PERFORMANCE.md index 633164b7..62b32a6d 100644 --- a/middleware/docs/PERFORMANCE.md +++ b/middleware/docs/PERFORMANCE.md @@ -203,87 +203,3 @@ use(req, res, next) { } ``` Always call `next()` (or send a response) on every code path. - ---- - -## Middleware Performance Benchmarks - -This package includes automated performance benchmarking to measure the latency -overhead of each middleware individually. Benchmarks establish a baseline with -no middleware, then measure the performance impact of adding each middleware -component. - -### Running Benchmarks - -```bash -# Run all middleware benchmarks -npm run benchmark - -# Run benchmarks with CI-friendly output -npm run benchmark:ci -``` - -### Benchmark Configuration - -- **Load**: 100 concurrent connections for 5 seconds -- **Protocol**: HTTP/1.1 with keep-alive -- **Headers**: Includes Authorization header for auth middleware testing -- **Endpoint**: Simple JSON response (`GET /test`) -- **Metrics**: Requests/second, latency percentiles (p50, p95, p99), error rate - -### Sample Output - -``` -๐Ÿš€ Starting Middleware Performance Benchmarks - -Configuration: 100 concurrent connections, 5s duration - -๐Ÿ“Š Running baseline benchmark (no middleware)... -๐Ÿ“Š Running benchmark for JWT Auth... -๐Ÿ“Š Running benchmark for RBAC... -๐Ÿ“Š Running benchmark for Security Headers... -๐Ÿ“Š Running benchmark for Timeout (5s)... -๐Ÿ“Š Running benchmark for Circuit Breaker... -๐Ÿ“Š Running benchmark for Correlation ID... - -๐Ÿ“ˆ Benchmark Results Summary -================================================================================ -โ”‚ Middleware โ”‚ Req/sec โ”‚ Avg Lat โ”‚ P95 Lat โ”‚ Overhead โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Baseline (No Middleware)โ”‚ 1250.5 โ”‚ 78.2 โ”‚ 125.8 โ”‚ 0% โ”‚ -โ”‚ JWT Auth โ”‚ 1189.3 โ”‚ 82.1 โ”‚ 132.4 โ”‚ 5% โ”‚ -โ”‚ RBAC โ”‚ 1215.7 โ”‚ 80.5 โ”‚ 128.9 โ”‚ 3% โ”‚ -โ”‚ Security Headers โ”‚ 1245.2 โ”‚ 78.8 โ”‚ 126.1 โ”‚ 0% โ”‚ -โ”‚ Timeout (5s) โ”‚ 1198.6 โ”‚ 81.2 โ”‚ 130.7 โ”‚ 4% โ”‚ -โ”‚ Circuit Breaker โ”‚ 1221.4 โ”‚ 79.8 โ”‚ 127.5 โ”‚ 2% โ”‚ -โ”‚ Correlation ID โ”‚ 1248.9 โ”‚ 78.4 โ”‚ 126.2 โ”‚ 0% โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -๐Ÿ“ Notes: -- Overhead is calculated as reduction in requests/second vs baseline -- Lower overhead percentage = better performance -- Results may vary based on system configuration -- Run with --ci flag for CI-friendly output -``` - -### Interpreting Results - -- **Overhead**: Percentage reduction in throughput compared to baseline -- **Latency**: Response time percentiles (lower is better) -- **Errors**: Number of failed requests during the test - -Use these benchmarks to: -- Compare middleware performance across versions -- Identify performance regressions -- Make informed decisions about middleware stacking -- Set performance budgets for new middleware - -### Implementation Details - -The benchmark system: -- Creates isolated Express applications for each middleware configuration -- Uses a simple load testing client (upgradeable to autocannon) -- Measures both throughput and latency characteristics -- Provides consistent, reproducible results - -See [benchmark.ts](../scripts/benchmark.ts) for implementation details. diff --git a/middleware/docs/PLUGINS.md b/middleware/docs/PLUGINS.md deleted file mode 100644 index 3d0b0391..00000000 --- a/middleware/docs/PLUGINS.md +++ /dev/null @@ -1,651 +0,0 @@ -# Plugin System Documentation - -## Overview - -The **External Plugin Loader** allows you to dynamically load, manage, and activate middleware plugins from npm packages into the `@mindblock/middleware` package. This enables a flexible, extensible architecture where developers can create custom middleware as independent npm packages. - -## Table of Contents - -- [Quick Start](#quick-start) -- [Plugin Architecture](#plugin-architecture) -- [Creating Plugins](#creating-plugins) -- [Loading Plugins](#loading-plugins) -- [Plugin Configuration](#plugin-configuration) -- [Plugin Lifecycle](#plugin-lifecycle) -- [Error Handling](#error-handling) -- [Examples](#examples) -- [Best Practices](#best-practices) - -## Quick Start - -### 1. Install the Plugin System - -The plugin system is built into `@mindblock/middleware`. No additional installation required. - -### 2. Load a Plugin - -```typescript -import { PluginRegistry } from '@mindblock/middleware'; - -// Create registry instance -const registry = new PluginRegistry({ - autoLoadEnabled: true, - middlewareVersion: '1.0.0' -}); - -// Initialize registry -await registry.init(); - -// Load a plugin -const loaded = await registry.load('@yourorg/plugin-example'); - -// Activate the plugin -await registry.activate(loaded.metadata.id); -``` - -### 3. Use Plugin Middleware - -```typescript -const app = express(); - -// Get all active plugin middlewares -const middlewares = registry.getAllMiddleware(); - -// Apply to your Express app -for (const [pluginId, middleware] of Object.entries(middlewares)) { - app.use(middleware); -} -``` - -## Plugin Architecture - -### Core Components - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ PluginRegistry โ”‚ -โ”‚ (High-level plugin management interface) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ PluginLoader โ”‚ -โ”‚ (Low-level plugin loading & lifecycle) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ PluginInterface (implements) โ”‚ -โ”‚ - Metadata โ”‚ -โ”‚ - Lifecycle Hooks โ”‚ -โ”‚ - Middleware Export โ”‚ -โ”‚ - Configuration Validation โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### Plugin Interface - -All plugins must implement the `PluginInterface`: - -```typescript -interface PluginInterface { - // Required - metadata: PluginMetadata; - - // Optional Lifecycle Hooks - onLoad?(context: PluginContext): Promise; - onInit?(config: PluginConfig, context: PluginContext): Promise; - onActivate?(context: PluginContext): Promise; - onDeactivate?(context: PluginContext): Promise; - onUnload?(context: PluginContext): Promise; - onReload?(config: PluginConfig, context: PluginContext): Promise; - - // Optional Methods - getMiddleware?(): NestMiddleware | ExpressMiddleware; - getExports?(): Record; - validateConfig?(config: PluginConfig): ValidationResult; - getDependencies?(): string[]; -} -``` - -## Creating Plugins - -### Step 1: Set Up Your Plugin Project - -```bash -mkdir @yourorg/plugin-example -cd @yourorg/plugin-example -npm init -y -npm install @nestjs/common express @mindblock/middleware typescript -npm install -D ts-node @types/express @types/node -``` - -### Step 2: Implement Your Plugin - -Create `src/index.ts`: - -```typescript -import { Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { - PluginInterface, - PluginMetadata, - PluginConfig, - PluginContext -} from '@mindblock/middleware'; - -export class MyPlugin implements PluginInterface { - private readonly logger = new Logger('MyPlugin'); - - metadata: PluginMetadata = { - id: 'com.yourorg.plugin.example', - name: 'My Custom Plugin', - description: 'A custom middleware plugin', - version: '1.0.0', - author: 'Your Organization', - homepage: 'https://github.com/yourorg/plugin-example', - license: 'MIT', - priority: 10 - }; - - async onLoad(context: PluginContext) { - this.logger.log('Plugin loaded'); - } - - async onInit(config: PluginConfig, context: PluginContext) { - this.logger.log('Plugin initialized', config); - } - - async onActivate(context: PluginContext) { - this.logger.log('Plugin activated'); - } - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - // Your middleware logic - res.setHeader('X-My-Plugin', 'active'); - next(); - }; - } - - validateConfig(config: PluginConfig) { - const errors: string[] = []; - // Validation logic - return { valid: errors.length === 0, errors }; - } -} - -export default MyPlugin; -``` - -### Step 3: Configure package.json - -Add `mindblockPlugin` configuration: - -```json -{ - "name": "@yourorg/plugin-example", - "version": "1.0.0", - "description": "Example middleware plugin", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "license": "MIT", - "keywords": ["mindblock", "plugin", "middleware"], - "mindblockPlugin": { - "version": "^1.0.0", - "priority": 10, - "autoLoad": false, - "configSchema": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true - } - } - } - }, - "dependencies": { - "@nestjs/common": "^11.0.0", - "@mindblock/middleware": "^1.0.0", - "express": "^5.0.0" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -``` - -### Step 4: Build and Publish - -```bash -npm run build -npm publish --access=public -``` - -## Loading Plugins - -### Manual Loading - -```typescript -const registry = new PluginRegistry(); -await registry.init(); - -// Load plugin -const plugin = await registry.load('@yourorg/plugin-example'); - -// Initialize with config -await registry.initialize(plugin.metadata.id, { - enabled: true, - options: { /* plugin-specific options */ } -}); - -// Activate -await registry.activate(plugin.metadata.id); -``` - -### Auto-Loading - -```typescript -const registry = new PluginRegistry({ - autoLoadPlugins: [ - '@yourorg/plugin-example', - '@yourorg/plugin-another' - ], - autoLoadEnabled: true -}); - -await registry.init(); // Plugins load automatically -``` - -###Discovery - -```typescript -// Discover available plugins in node_modules -const discovered = await registry.loader.discoverPlugins(); -console.log('Available plugins:', discovered); -``` - -## Plugin Configuration - -### Configuration Schema - -Plugins can define JSON Schema for configuration validation: - -```typescript -metadata: PluginMetadata = { - id: 'com.example.plugin', - // ... - configSchema: { - type: 'object', - required: ['someRequired'], - properties: { - enabled: { type: 'boolean', default: true }, - someRequired: { type: 'string' }, - timeout: { type: 'number', minimum: 1000 } - } - } -}; -``` - -### Validating Configuration - -```typescript -const config: PluginConfig = { - enabled: true, - options: { someRequired: 'value', timeout: 5000 } -}; - -const result = registry.validateConfig(pluginId, config); -if (!result.valid) { - console.error('Invalid config:', result.errors); -} -``` - -## Plugin Lifecycle - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Plugin Lifecycle Flow โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - - load() - โ”‚ - โ–ผ - onLoad() โ”€โ”€โ–บ Initialization validation - โ”‚ - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ โ”‚ - init() manual config - โ”‚ โ”‚ - โ–ผ โ–ผ - onInit() โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ - activate() - โ”‚ - โ–ผ - onActivate() โ”€โ”€โ–บ Plugin ready & active - โ”‚ - โ”‚ (optionally) - โ”œโ”€โ–บ reload() โ”€โ”€โ–บ onReload() - โ”‚ - โ–ผ (eventually) - deactivate() - โ”‚ - โ–ผ - onDeactivate() - โ”‚ - โ–ผ - unload() - โ”‚ - โ–ผ - onUnload() - โ”‚ - โ–ผ - โœ“ Removed -``` - -### Lifecycle Hooks - -| Hook | When Called | Purpose | -|------|-------------|---------| -| `onLoad` | After module import | Validate dependencies, setup | -| `onInit` | After configuration merge | Initialize with config | -| `onActivate` | When activated | Start services, open connections | -| `onDeactivate` | When deactivated | Stop services, cleanup | -| `onUnload` | Before removal | Final cleanup | -| `onReload` | On configuration change | Update configuration without unloading | - -## Error Handling - -### Error Types - -```typescript -// Plugin not found -try { - registry.getPluginOrThrow('unknown-plugin'); -} catch (error) { - if (error instanceof PluginNotFoundError) { - console.error('Plugin not found'); - } -} - -// Plugin already loaded -catch (error) { - if (error instanceof PluginAlreadyLoadedError) { - console.error('Plugin already loaded'); - } -} - -// Invalid configuration -catch (error) { - if (error instanceof PluginConfigError) { - console.error('Invalid config:', error.details); - } -} - -// Unmet dependencies -catch (error) { - if (error instanceof PluginDependencyError) { - console.error('Missing dependencies'); - } -} - -// Version mismatch -catch (error) { - if (error instanceof PluginVersionError) { - console.error('Version incompatible'); - } -} -``` - -## Examples - -### Example 1: Rate Limiting Plugin - -```typescript -export class RateLimitPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.rate-limit', - name: 'Rate Limiting', - version: '1.0.0', - description: 'Rate limiting middleware' - }; - - private store = new Map(); - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const key = req.ip; - const now = Date.now(); - const windowMs = 60 * 1000; - - if (!this.store.has(key)) { - this.store.set(key, []); - } - - const timestamps = this.store.get(key)!; - const recentRequests = timestamps.filter(t => now - t < windowMs); - - if (recentRequests.length > 100) { - return res.status(429).json({ error: 'Too many requests' }); - } - - recentRequests.push(now); - this.store.set(key, recentRequests); - - next(); - }; - } -} -``` - -### Example 2: Logging Plugin with Configuration - -```typescript -export class LoggingPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.logging', - name: 'Request Logging', - version: '1.0.0', - description: 'Log all HTTP requests', - configSchema: { - properties: { - logLevel: { type: 'string', enum: ['debug', 'info', 'warn', 'error'] }, - excludePaths: { type: 'array', items: { type: 'string' } } - } - } - }; - - private config: PluginConfig; - - validateConfig(config: PluginConfig) { - if (config.options?.logLevel && !['debug', 'info', 'warn', 'error'].includes(config.options.logLevel)) { - return { valid: false, errors: ['Invalid logLevel'] }; - } - return { valid: true, errors: [] }; - } - - async onInit(config: PluginConfig) { - this.config = config; - } - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const excludePaths = this.config.options?.excludePaths || []; - if (!excludePaths.includes(req.path)) { - console.log(`[${this.config.options?.logLevel || 'info'}] ${req.method} ${req.path}`); - } - next(); - }; - } -} -``` - -## Best Practices - -### 1. Plugin Naming Convention - -- Use scoped package names: `@organization/plugin-feature` -- Use descriptive plugin IDs: `com.organization.plugin.feature` -- Include "plugin" in package and plugin names - -### 2. Version Management - -- Follow semantic versioning (semver) for your plugin -- Specify middleware version requirements in package.json -- Test against multiple middleware versions - -### 3. Configuration Validation - -```typescript -validateConfig(config: PluginConfig) { - const errors: string[] = []; - const warnings: string[] = []; - - if (!config.options?.require Field) { - errors.push('requiredField is required'); - } - - if (config.options?.someValue > 1000) { - warnings.push('someValue is unusually high'); - } - - return { valid: errors.length === 0, errors, warnings }; -} -``` - -### 4. Error Handling - -```typescript -async onInit(config: PluginConfig, context: PluginContext) { - try { - // Initialization logic - } catch (error) { - context.logger?.error(`Failed to initialize: ${error.message}`); - throw error; // Let framework handle it - } -} -``` - -### 5. Resource Cleanup - -```typescript -private connections: any[] = []; - -async onActivate(context: PluginContext) { - // Open resources - this.connections.push(await openConnection()); -} - -async onDeactivate(context: PluginContext) { - // Close resources - for (const conn of this.connections) { - await conn.close(); - } - this.connections = []; -} -``` - -### 6. Dependencies - -```typescript -getDependencies(): string[] { - return [ - 'com.example.auth-plugin', // This plugin must load first - 'com.example.logging-plugin' - ]; -} -``` - -### 7. Documentation - -- Write clear README for your plugin -- Include configuration examples -- Document any external dependencies -- Provide troubleshooting guide -- Include integration examples - -### 8. Testing - -```typescript -describe('MyPlugin', () => { - let plugin: MyPlugin; - - beforeEach(() => { - plugin = new MyPlugin(); - }); - - it('should validate configuration', () => { - const result = plugin.validateConfig({ enabled: true }); - expect(result.valid).toBe(true); - }); - - it('should handle middleware requests', () => { - const middleware = plugin.getMiddleware(); - const req = {}, res = { setHeader: jest.fn() }, next = jest.fn(); - middleware(req as any, res as any, next); - expect(next).toHaveBeenCalled(); - }); -}); -``` - -## Advanced Topics - -### Priority-Based Execution - -Set plugin priority to control execution order: - -```typescript -metadata = { - // ... - priority: 10 // Higher = executes later -}; -``` - -### Plugin Communication - -Plugins can access other loaded plugins: - -```typescript -async getOtherPlugin(context: PluginContext) { - const otherPlugin = context.plugins?.get('com.example.other-plugin'); - const exports = otherPlugin?.instance.getExports?.(); - return exports; -} -``` - -### Runtime Configuration Updates - -Update plugin configuration without full reload: - -```typescript -await registry.reload(pluginId, { - enabled: true, - options: { /* new config */ } -}); -``` - -## Troubleshooting - -### Plugin Not Loading - -1. Check that npm package is installed: `npm list @yourorg/plugin-name` -2. Verify `main` field in plugin's package.json -3. Check that plugin exports a valid PluginInterface -4. Review logs for specific error messages - -### Configuration Errors - -1. Validate config against schema -2. Check required fields are present -3. Ensure all options match expected types - -### Permission Issues - -1. Check plugin version compatibility -2. Verify all dependencies are met -3. Check that required plugins are loaded first - ---- - -For more examples and details, see the [example plugin template](../src/plugins/example.plugin.ts). diff --git a/middleware/docs/PLUGIN_QUICKSTART.md b/middleware/docs/PLUGIN_QUICKSTART.md deleted file mode 100644 index c5cde301..00000000 --- a/middleware/docs/PLUGIN_QUICKSTART.md +++ /dev/null @@ -1,480 +0,0 @@ -# Plugin Development Quick Start Guide - -This guide walks you through creating your first middleware plugin for `@mindblock/middleware`. - -## 5-Minute Setup - -### 1. Create Plugin Project - -```bash -mkdir @myorg/plugin-awesome -cd @myorg/plugin-awesome -npm init -y -``` - -### 2. Install Dependencies - -```bash -npm install --save @nestjs/common express -npm install --save-dev typescript @types/express @types/node ts-node -``` - -### 3. Create Your Plugin - -Create `src/index.ts`: - -```typescript -import { Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { - PluginInterface, - PluginMetadata, - PluginConfig, - PluginContext -} from '@mindblock/middleware'; - -export class AwesomePlugin implements PluginInterface { - private readonly logger = new Logger('AwesomePlugin'); - - metadata: PluginMetadata = { - id: 'com.myorg.plugin.awesome', - name: 'Awesome Plugin', - description: 'My awesome middleware plugin', - version: '1.0.0', - author: 'Your Name', - license: 'MIT' - }; - - async onLoad() { - this.logger.log('Plugin loaded!'); - } - - async onActivate() { - this.logger.log('Plugin is now active'); - } - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - // Add your middleware logic - res.setHeader('X-Awesome-Plugin', 'true'); - next(); - }; - } - - validateConfig(config: PluginConfig) { - return { valid: true, errors: [] }; - } -} - -export default AwesomePlugin; -``` - -### 4. Update package.json - -```json -{ - "name": "@myorg/plugin-awesome", - "version": "1.0.0", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "license": "MIT", - "keywords": ["mindblock", "plugin", "middleware"], - "mindblockPlugin": { - "version": "^1.0.0", - "autoLoad": false - }, - "dependencies": { - "@nestjs/common": "^11.0.0", - "express": "^5.0.0" - }, - "devDependencies": { - "@types/express": "^5.0.0", - "@types/node": "^20.0.0", - "typescript": "^5.0.0" - } -} -``` - -### 5. Build and Test Locally - -```bash -# Build TypeScript -npx tsc src/index.ts --outDir dist --declaration - -# Test in your app -npm link -# In your app: npm link @myorg/plugin-awesome -``` - -### 6. Use Your Plugin - -```typescript -import { PluginRegistry } from '@mindblock/middleware'; - -const registry = new PluginRegistry(); -await registry.init(); - -// Load your local plugin -const plugin = await registry.load('@myorg/plugin-awesome'); -await registry.initialize(plugin.metadata.id); -await registry.activate(plugin.metadata.id); - -// Get the middleware -const middleware = registry.getMiddleware(plugin.metadata.id); -app.use(middleware); -``` - -## Common Plugin Patterns - -### Pattern 1: Configuration-Based Plugin - -```typescript -export class ConfigurablePlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.configurable', - // ... - configSchema: { - type: 'object', - properties: { - enabled: { type: 'boolean', default: true }, - timeout: { type: 'number', minimum: 1000, default: 5000 }, - excludePaths: { type: 'array', items: { type: 'string' } } - } - } - }; - - private timeout = 5000; - private excludePaths: string[] = []; - - async onInit(config: PluginConfig) { - if (config.options) { - this.timeout = config.options.timeout ?? 5000; - this.excludePaths = config.options.excludePaths ?? []; - } - } - - validateConfig(config: PluginConfig) { - const errors: string[] = []; - if (config.options?.timeout && config.options.timeout < 1000) { - errors.push('timeout must be at least 1000ms'); - } - return { valid: errors.length === 0, errors }; - } - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - // Use configuration - if (!this.excludePaths.includes(req.path)) { - // Apply middleware with this.timeout - } - next(); - }; - } -} -``` - -### Pattern 2: Stateful Plugin with Resource Management - -```typescript -export class StatefulPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.stateful', - // ... - }; - - private connections: Database[] = []; - - async onActivate(context: PluginContext) { - // Open resources - const db = await Database.connect(); - this.connections.push(db); - context.logger?.log('Database connected'); - } - - async onDeactivate(context: PluginContext) { - // Close resources - for (const conn of this.connections) { - await conn.close(); - } - this.connections = []; - context.logger?.log('Database disconnected'); - } - - getMiddleware() { - return async (req: Request, res: Response, next: NextFunction) => { - // Use this.connections - const result = await this.connections[0].query('SELECT 1'); - next(); - }; - } -} -``` - -### Pattern 3: Plugin with Dependencies - -```typescript -export class DependentPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.dependent', - // ... - }; - - getDependencies(): string[] { - return ['com.example.auth-plugin']; // Must load after auth plugin - } - - async onInit(config: PluginConfig, context: PluginContext) { - // Get the auth plugin - const authPlugin = context.plugins?.get('com.example.auth-plugin'); - const authExports = authPlugin?.instance.getExports?.(); - // Use auth exports - } - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - // Middleware that depends on auth plugin - next(); - }; - } -} -``` - -### Pattern 4: Plugin with Custom Exports - -```typescript -export class UtilityPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.utility', - // ... - }; - - private cache = new Map(); - - getExports() { - return { - cache: this.cache, - clearCache: () => this.cache.clear(), - getValue: (key: string) => this.cache.get(key), - setValue: (key: string, value: any) => this.cache.set(key, value) - }; - } - - // Other plugins can now use these exports: - // const exports = registry.getExports('com.example.utility'); - // exports.setValue('key', 'value'); -} -``` - -## Testing Your Plugin - -Create `test/plugin.spec.ts`: - -```typescript -import { AwesomePlugin } from '../src/index'; -import { PluginContext } from '@mindblock/middleware'; - -describe('AwesomePlugin', () => { - let plugin: AwesomePlugin; - - beforeEach(() => { - plugin = new AwesomePlugin(); - }); - - it('should have valid metadata', () => { - expect(plugin.metadata).toBeDefined(); - expect(plugin.metadata.id).toBe('com.myorg.plugin.awesome'); - }); - - it('should validate config', () => { - const result = plugin.validateConfig({ enabled: true }); - expect(result.valid).toBe(true); - }); - - it('should provide middleware', () => { - const middleware = plugin.getMiddleware(); - expect(typeof middleware).toBe('function'); - - const res = { setHeader: jest.fn() }; - const next = jest.fn(); - middleware({} as any, res as any, next); - - expect(res.setHeader).toHaveBeenCalledWith('X-Awesome-Plugin', 'true'); - expect(next).toHaveBeenCalled(); - }); - - it('should execute lifecycle hooks', async () => { - const context: PluginContext = { logger: console }; - - await expect(plugin.onLoad?.(context)).resolves.not.toThrow(); - await expect(plugin.onActivate?.(context)).resolves.not.toThrow(); - }); -}); -``` - -Run tests: - -```bash -npm install --save-dev jest ts-jest @types/jest -npm test -``` - -## Publishing Your Plugin - -### 1. Create GitHub Repository - -```bash -git init -git add . -git commit -m "Initial commit: Awesome Plugin" -git remote add origin https://github.com/yourorg/plugin-awesome.git -git push -u origin main -``` - -### 2. Publish to npm - -```bash -# Login to npm -npm login - -# Publish (for scoped packages with --access=public) -npm publish --access=public -``` - -### 3. Add to Plugin Registry - -Users can now install and use your plugin: - -```bash -npm install @myorg/plugin-awesome -``` - -```typescript -const registry = new PluginRegistry(); -await registry.init(); -await registry.loadAndActivate('@myorg/plugin-awesome'); -``` - -## Plugin Checklist - -Before publishing, ensure: - -- โœ… Plugin implements `PluginInterface` -- โœ… Metadata includes all required fields (id, name, version, description) -- โœ… Configuration validates correctly -- โœ… Lifecycle hooks handle errors gracefully -- โœ… Resource cleanup in `onDeactivate` and `onUnload` -- โœ… Tests pass (>80% coverage recommended) -- โœ… TypeScript compiles without errors -- โœ… README with setup and usage examples -- โœ… package.json includes `mindblockPlugin` configuration -- โœ… Scoped package name (e.g., `@org/plugin-name`) - -## Example Plugins - -### Example 1: CORS Plugin - -```typescript -export class CorsPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.cors', - name: 'CORS Handler', - version: '1.0.0', - description: 'Handle CORS headers' - }; - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - - if (req.method === 'OPTIONS') { - return res.sendStatus(200); - } - - next(); - }; - } -} -``` - -### Example 2: Request ID Plugin - -```typescript -import { v4 as uuidv4 } from 'uuid'; - -export class RequestIdPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'com.example.request-id', - name: 'Request ID Generator', - version: '1.0.0', - description: 'Add unique ID to each request' - }; - - getMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const requestId = req.headers['x-request-id'] || uuidv4(); - res.setHeader('X-Request-ID', requestId); - (req as any).id = requestId; - next(); - }; - } - - getExports() { - return { - getRequestId: (req: Request) => (req as any).id - }; - } -} -``` - -## Advanced Topics - -### Accessing Plugin Context - -```typescript -async onInit(config: PluginConfig, context: PluginContext) { - // Access logger - context.logger?.log('Initializing plugin'); - - // Access environment - const apiKey = context.env?.API_KEY; - - // Access other plugins - const otherPlugin = context.plugins?.get('com.example.other'); - - // Access app config - const appConfig = context.config; -} -``` - -### Plugin-to-Plugin Communication - -```typescript -// Plugin A -getExports() { - return { - getUserData: (userId: string) => ({ id: userId, name: 'John' }) - }; -} - -// Plugin B -async onInit(config: PluginConfig, context: PluginContext) { - const pluginA = context.plugins?.get('com.example.plugin-a'); - const moduleA = pluginA?.instance.getExports?.(); - const userData = moduleA?.getUserData('123'); -} -``` - -## Resources - -- [Full Plugin Documentation](PLUGINS.md) -- [Plugin API Reference](../src/common/interfaces/plugin.interface.ts) -- [Example Plugin](../src/plugins/example.plugin.ts) -- [Plugin System Tests](../tests/integration/plugin-system.integration.spec.ts) - ---- - -**Happy plugin development!** ๐Ÿš€ - -Have questions? Check the [main documentation](PLUGINS.md) or create an issue. diff --git a/middleware/package.json b/middleware/package.json index 64bede7f..0ba0c3a3 100644 --- a/middleware/package.json +++ b/middleware/package.json @@ -13,9 +13,7 @@ "lint": "eslint -c eslint.config.mjs \"src/**/*.ts\" \"tests/**/*.ts\"", "lint:fix": "eslint -c eslint.config.mjs \"src/**/*.ts\" \"tests/**/*.ts\" --fix", "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", - "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", - "benchmark": "ts-node scripts/benchmark.ts", - "benchmark:ci": "ts-node scripts/benchmark.ts --ci" + "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"" }, "dependencies": { "@nestjs/common": "^11.0.12", @@ -27,24 +25,20 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "micromatch": "^4.0.8", - "semver": "^7.6.0", "stellar-sdk": "^13.1.0" }, "devDependencies": { "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", - "@types/semver": "^7.5.8", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", - "autocannon": "^7.15.0", "eslint": "^9.18.0", "eslint-plugin-prettier": "^5.2.2", "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", "typescript": "^5.7.3", "typescript-eslint": "^8.20.0" } diff --git a/middleware/scripts/benchmark.ts b/middleware/scripts/benchmark.ts deleted file mode 100644 index b31cf6d0..00000000 --- a/middleware/scripts/benchmark.ts +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env ts-node - -import http from 'http'; -import express, { Request, Response, NextFunction } from 'express'; -import { Server } from 'http'; - -// Import middleware -import { SecurityHeadersMiddleware } from '../src/security/security-headers.middleware'; -import { TimeoutMiddleware } from '../src/middleware/advanced/timeout.middleware'; -import { CircuitBreakerMiddleware, CircuitBreakerService } from '../src/middleware/advanced/circuit-breaker.middleware'; -import { CorrelationIdMiddleware } from '../src/monitoring/correlation-id.middleware'; -import { unless } from '../src/middleware/utils/conditional.middleware'; - -interface BenchmarkResult { - middleware: string; - requestsPerSecond: number; - latency: { - average: number; - p50: number; - p95: number; - p99: number; - }; - errors: number; -} - -interface MiddlewareConfig { - name: string; - middleware: any; - options?: any; -} - -// Simple load testing function to replace autocannon -async function simpleLoadTest(url: string, options: { - connections: number; - duration: number; - headers?: Record; -}): Promise<{ - requests: { average: number }; - latency: { average: number; p50: number; p95: number; p99: number }; - errors: number; -}> { - const { connections, duration, headers = {} } = options; - const latencies: number[] = []; - let completedRequests = 0; - let errors = 0; - const startTime = Date.now(); - - // Create concurrent requests - const promises = Array.from({ length: connections }, async () => { - const requestStart = Date.now(); - - try { - await new Promise((resolve, reject) => { - const req = http.request(url, { - method: 'GET', - headers - }, (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - completedRequests++; - latencies.push(Date.now() - requestStart); - resolve(); - }); - }); - - req.on('error', (err) => { - errors++; - latencies.push(Date.now() - requestStart); - reject(err); - }); - - req.setTimeout(10000, () => { - errors++; - latencies.push(Date.now() - requestStart); - req.destroy(); - reject(new Error('Timeout')); - }); - - req.end(); - }); - } catch (error) { - // Ignore errors for load testing - } - }); - - // Run for the specified duration - await Promise.race([ - Promise.all(promises), - new Promise(resolve => setTimeout(resolve, duration * 1000)) - ]); - - const totalTime = (Date.now() - startTime) / 1000; // in seconds - const requestsPerSecond = completedRequests / totalTime; - - // Calculate percentiles - latencies.sort((a, b) => a - b); - const p50 = latencies[Math.floor(latencies.length * 0.5)] || 0; - const p95 = latencies[Math.floor(latencies.length * 0.95)] || 0; - const p99 = latencies[Math.floor(latencies.length * 0.99)] || 0; - const average = latencies.reduce((sum, lat) => sum + lat, 0) / latencies.length || 0; - - return { - requests: { average: requestsPerSecond }, - latency: { average, p50, p95, p99 }, - errors - }; -} - -// Mock JWT Auth Middleware (simplified for benchmarking) -class MockJwtAuthMiddleware { - constructor(private options: { secret: string; algorithms?: string[] }) {} - - use(req: Request, res: Response, next: NextFunction) { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ error: 'No token provided' }); - } - - // For benchmarking, just check if a token is present (skip actual verification) - const token = authHeader.substring(7); - if (!token || token.length < 10) { - return res.status(401).json({ error: 'Invalid token' }); - } - - // Mock user object - (req as any).user = { - userId: '1234567890', - email: 'test@example.com', - userRole: 'user' - }; - next(); - } -} - -// Mock RBAC Middleware (simplified for benchmarking) -class MockRbacMiddleware { - constructor(private options: { roles: string[]; defaultRole: string }) {} - - use(req: Request, res: Response, next: NextFunction) { - const user = (req as any).user; - if (!user) { - return res.status(401).json({ error: 'No user found' }); - } - - // Simple role check - allow if user has any of the allowed roles - const userRole = user.userRole || this.options.defaultRole; - if (!this.options.roles.includes(userRole)) { - return res.status(403).json({ error: 'Insufficient permissions' }); - } - - next(); - } -} - -class MiddlewareBenchmarker { - private port = 3001; - private server: Server | null = null; - - private middlewareConfigs: MiddlewareConfig[] = [ - { - name: 'JWT Auth', - middleware: MockJwtAuthMiddleware, - options: { - secret: 'test-secret-key-for-benchmarking-only', - algorithms: ['HS256'] - } - }, - { - name: 'RBAC', - middleware: MockRbacMiddleware, - options: { - roles: ['user', 'admin'], - defaultRole: 'user' - } - }, - { - name: 'Security Headers', - middleware: SecurityHeadersMiddleware, - options: {} - }, - { - name: 'Timeout (5s)', - middleware: TimeoutMiddleware, - options: { timeout: 5000 } - }, - { - name: 'Circuit Breaker', - middleware: CircuitBreakerMiddleware, - options: { - failureThreshold: 5, - recoveryTimeout: 30000, - monitoringPeriod: 10000 - } - }, - { - name: 'Correlation ID', - middleware: CorrelationIdMiddleware, - options: {} - } - ]; - - async runBenchmarks(): Promise { - console.log('๐Ÿš€ Starting Middleware Performance Benchmarks\n'); - console.log('Configuration: 100 concurrent connections, 5s duration\n'); - - const results: BenchmarkResult[] = []; - - // Baseline benchmark (no middleware) - console.log('๐Ÿ“Š Running baseline benchmark (no middleware)...'); - const baselineResult = await this.runBenchmark([]); - results.push({ - middleware: 'Baseline (No Middleware)', - ...baselineResult - }); - - // Individual middleware benchmarks - for (const config of this.middlewareConfigs) { - console.log(`๐Ÿ“Š Running benchmark for ${config.name}...`); - try { - const result = await this.runBenchmark([config]); - results.push({ - middleware: config.name, - ...result - }); - } catch (error) { - console.error(`โŒ Failed to benchmark ${config.name}:`, error.message); - results.push({ - middleware: config.name, - requestsPerSecond: 0, - latency: { average: 0, p50: 0, p95: 0, p99: 0 }, - errors: 0 - }); - } - } - - this.displayResults(results); - } - - private async runBenchmark(middlewareConfigs: MiddlewareConfig[]): Promise> { - const app = express(); - - // Simple test endpoint - app.get('/test', (req: Request, res: Response) => { - res.json({ message: 'ok', timestamp: Date.now() }); - }); - - // Apply middleware - for (const config of middlewareConfigs) { - if (config.middleware) { - // Special handling for CircuitBreakerMiddleware - if (config.middleware === CircuitBreakerMiddleware) { - const circuitBreakerService = new CircuitBreakerService(config.options); - const instance = new CircuitBreakerMiddleware(circuitBreakerService); - app.use((req, res, next) => instance.use(req, res, next)); - } - // For middleware that need instantiation - else if (typeof config.middleware === 'function' && config.middleware.prototype?.use) { - const instance = new (config.middleware as any)(config.options); - app.use((req, res, next) => instance.use(req, res, next)); - } else if (typeof config.middleware === 'function') { - // For functional middleware - app.use(config.middleware(config.options)); - } - } - } - - // Start server - this.server = app.listen(this.port); - - try { - // Run simple load test - const result = await simpleLoadTest(`http://localhost:${this.port}/test`, { - connections: 100, - duration: 5, // 5 seconds instead of 10 for faster testing - headers: { - 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }); - - return { - requestsPerSecond: Math.round(result.requests.average * 100) / 100, - latency: { - average: Math.round(result.latency.average * 100) / 100, - p50: Math.round(result.latency.p50 * 100) / 100, - p95: Math.round(result.latency.p95 * 100) / 100, - p99: Math.round(result.latency.p99 * 100) / 100 - }, - errors: result.errors - }; - } finally { - // Clean up server - if (this.server) { - this.server.close(); - this.server = null; - } - } - } - - private displayResults(results: BenchmarkResult[]): void { - console.log('\n๐Ÿ“ˆ Benchmark Results Summary'); - console.log('=' .repeat(80)); - - console.log('โ”‚ Middleware'.padEnd(25) + 'โ”‚ Req/sec'.padEnd(10) + 'โ”‚ Avg Lat'.padEnd(10) + 'โ”‚ P95 Lat'.padEnd(10) + 'โ”‚ Overhead'.padEnd(12) + 'โ”‚'); - console.log('โ”œ' + 'โ”€'.repeat(24) + 'โ”ผ' + 'โ”€'.repeat(9) + 'โ”ผ' + 'โ”€'.repeat(9) + 'โ”ผ' + 'โ”€'.repeat(9) + 'โ”ผ' + 'โ”€'.repeat(11) + 'โ”ค'); - - const baseline = results.find(r => r.middleware === 'Baseline (No Middleware)'); - if (!baseline) { - console.error('โŒ Baseline benchmark not found!'); - return; - } - - for (const result of results) { - const overhead = result.middleware === 'Baseline (No Middleware)' - ? '0%' - : result.requestsPerSecond > 0 - ? `${Math.round((1 - result.requestsPerSecond / baseline.requestsPerSecond) * 100)}%` - : 'N/A'; - - console.log( - 'โ”‚ ' + result.middleware.padEnd(23) + ' โ”‚ ' + - result.requestsPerSecond.toString().padEnd(8) + ' โ”‚ ' + - result.latency.average.toString().padEnd(8) + ' โ”‚ ' + - result.latency.p95.toString().padEnd(8) + ' โ”‚ ' + - overhead.padEnd(10) + ' โ”‚' - ); - } - - console.log('โ””' + 'โ”€'.repeat(24) + 'โ”ด' + 'โ”€'.repeat(9) + 'โ”ด' + 'โ”€'.repeat(9) + 'โ”ด' + 'โ”€'.repeat(9) + 'โ”ด' + 'โ”€'.repeat(11) + 'โ”˜'); - - console.log('\n๐Ÿ“ Notes:'); - console.log('- Overhead is calculated as reduction in requests/second vs baseline'); - console.log('- Lower overhead percentage = better performance'); - console.log('- Results may vary based on system configuration'); - console.log('- Run with --ci flag for CI-friendly output'); - } -} - -// CLI handling -async function main() { - const isCI = process.argv.includes('--ci'); - - try { - const benchmarker = new MiddlewareBenchmarker(); - await benchmarker.runBenchmarks(); - } catch (error) { - console.error('โŒ Benchmark failed:', error); - process.exit(1); - } -} - -if (require.main === module) { - main(); -} \ No newline at end of file diff --git a/middleware/src/common/interfaces/index.ts b/middleware/src/common/interfaces/index.ts deleted file mode 100644 index 4c094b58..00000000 --- a/middleware/src/common/interfaces/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Plugin interfaces and error types -export * from './plugin.interface'; -export * from './plugin.errors'; diff --git a/middleware/src/common/interfaces/plugin.errors.ts b/middleware/src/common/interfaces/plugin.errors.ts deleted file mode 100644 index ff6cbaae..00000000 --- a/middleware/src/common/interfaces/plugin.errors.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Base error class for plugin-related errors. - */ -export class PluginError extends Error { - constructor(message: string, public readonly code: string = 'PLUGIN_ERROR', public readonly details?: any) { - super(message); - this.name = 'PluginError'; - Object.setPrototypeOf(this, PluginError.prototype); - } -} - -/** - * Error thrown when a plugin is not found. - */ -export class PluginNotFoundError extends PluginError { - constructor(pluginId: string, details?: any) { - super(`Plugin not found: ${pluginId}`, 'PLUGIN_NOT_FOUND', details); - this.name = 'PluginNotFoundError'; - Object.setPrototypeOf(this, PluginNotFoundError.prototype); - } -} - -/** - * Error thrown when a plugin fails to load due to missing module or import error. - */ -export class PluginLoadError extends PluginError { - constructor(pluginId: string, reason?: string, details?: any) { - super( - `Failed to load plugin: ${pluginId}${reason ? ` - ${reason}` : ''}`, - 'PLUGIN_LOAD_ERROR', - details - ); - this.name = 'PluginLoadError'; - Object.setPrototypeOf(this, PluginLoadError.prototype); - } -} - -/** - * Error thrown when a plugin is already loaded. - */ -export class PluginAlreadyLoadedError extends PluginError { - constructor(pluginId: string, details?: any) { - super(`Plugin already loaded: ${pluginId}`, 'PLUGIN_ALREADY_LOADED', details); - this.name = 'PluginAlreadyLoadedError'; - Object.setPrototypeOf(this, PluginAlreadyLoadedError.prototype); - } -} - -/** - * Error thrown when plugin configuration is invalid. - */ -export class PluginConfigError extends PluginError { - constructor(pluginId: string, errors: string[], details?: any) { - super( - `Invalid configuration for plugin: ${pluginId}\n${errors.join('\n')}`, - 'PLUGIN_CONFIG_ERROR', - details - ); - this.name = 'PluginConfigError'; - Object.setPrototypeOf(this, PluginConfigError.prototype); - } -} - -/** - * Error thrown when plugin dependencies are not met. - */ -export class PluginDependencyError extends PluginError { - constructor(pluginId: string, missingDependencies: string[], details?: any) { - super( - `Plugin dependencies not met for: ${pluginId} - Missing: ${missingDependencies.join(', ')}`, - 'PLUGIN_DEPENDENCY_ERROR', - details - ); - this.name = 'PluginDependencyError'; - Object.setPrototypeOf(this, PluginDependencyError.prototype); - } -} - -/** - * Error thrown when plugin version is incompatible. - */ -export class PluginVersionError extends PluginError { - constructor( - pluginId: string, - required: string, - actual: string, - details?: any - ) { - super( - `Plugin version mismatch: ${pluginId} requires ${required} but got ${actual}`, - 'PLUGIN_VERSION_ERROR', - details - ); - this.name = 'PluginVersionError'; - Object.setPrototypeOf(this, PluginVersionError.prototype); - } -} - -/** - * Error thrown when plugin initialization fails. - */ -export class PluginInitError extends PluginError { - constructor(pluginId: string, reason?: string, details?: any) { - super( - `Failed to initialize plugin: ${pluginId}${reason ? ` - ${reason}` : ''}`, - 'PLUGIN_INIT_ERROR', - details - ); - this.name = 'PluginInitError'; - Object.setPrototypeOf(this, PluginInitError.prototype); - } -} - -/** - * Error thrown when trying to operate on an inactive plugin. - */ -export class PluginInactiveError extends PluginError { - constructor(pluginId: string, details?: any) { - super(`Plugin is not active: ${pluginId}`, 'PLUGIN_INACTIVE', details); - this.name = 'PluginInactiveError'; - Object.setPrototypeOf(this, PluginInactiveError.prototype); - } -} - -/** - * Error thrown when plugin package.json is invalid. - */ -export class InvalidPluginPackageError extends PluginError { - constructor(packagePath: string, errors: string[], details?: any) { - super( - `Invalid plugin package.json at ${packagePath}:\n${errors.join('\n')}`, - 'INVALID_PLUGIN_PACKAGE', - details - ); - this.name = 'InvalidPluginPackageError'; - Object.setPrototypeOf(this, InvalidPluginPackageError.prototype); - } -} - -/** - * Error thrown when npm package resolution fails. - */ -export class PluginResolutionError extends PluginError { - constructor(pluginName: string, reason?: string, details?: any) { - super( - `Failed to resolve plugin package: ${pluginName}${reason ? ` - ${reason}` : ''}`, - 'PLUGIN_RESOLUTION_ERROR', - details - ); - this.name = 'PluginResolutionError'; - Object.setPrototypeOf(this, PluginResolutionError.prototype); - } -} diff --git a/middleware/src/common/interfaces/plugin.interface.ts b/middleware/src/common/interfaces/plugin.interface.ts deleted file mode 100644 index 73cb974c..00000000 --- a/middleware/src/common/interfaces/plugin.interface.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; - -/** - * Semantic version constraint for plugin compatibility. - * Supports semver ranges like "^1.0.0", "~1.2.0", "1.x", etc. - */ -export type VersionConstraint = string; - -/** - * Metadata about the plugin. - */ -export interface PluginMetadata { - /** Unique identifier for the plugin (e.g., @mindblock/plugin-rate-limit) */ - id: string; - - /** Display name of the plugin */ - name: string; - - /** Short description of what the plugin does */ - description: string; - - /** Current version of the plugin (must follow semver) */ - version: string; - - /** Plugin author or organization */ - author?: string; - - /** URL for the plugin's GitHub repository, documentation, or home page */ - homepage?: string; - - /** License identifier (e.g., MIT, Apache-2.0) */ - license?: string; - - /** List of keywords for discoverability */ - keywords?: string[]; - - /** Required middleware package version (e.g., "^1.0.0") */ - requiredMiddlewareVersion?: VersionConstraint; - - /** Execution priority: lower runs first, higher runs last (default: 0) */ - priority?: number; - - /** Whether this plugin should be loaded automatically */ - autoLoad?: boolean; - - /** Configuration schema for the plugin (JSON Schema format) */ - configSchema?: Record; - - /** Custom metadata */ - [key: string]: any; -} - -/** - * Plugin context provided during initialization. - * Gives plugin access to shared services and utilities. - */ -export interface PluginContext { - /** Logger instance for the plugin */ - logger?: any; - - /** Environment variables */ - env?: NodeJS.ProcessEnv; - - /** Application configuration */ - config?: Record; - - /** Access to other loaded plugins */ - plugins?: Map; - - /** Custom context data */ - [key: string]: any; -} - -/** - * Plugin configuration passed at runtime. - */ -export interface PluginConfig { - /** Whether the plugin is enabled */ - enabled?: boolean; - - /** Plugin-specific options */ - options?: Record; - - /** Custom metadata */ - [key: string]: any; -} - -/** - * Plugin lifecycle hooks. - */ -export interface PluginHooks { - /** - * Called when the plugin is being loaded. - * Useful for validation, setup, or dependency checks. - */ - onLoad?: (context: PluginContext) => Promise | void; - - /** - * Called when the plugin is being initialized with configuration. - */ - onInit?: (config: PluginConfig, context: PluginContext) => Promise | void; - - /** - * Called when the plugin is being activated for use. - */ - onActivate?: (context: PluginContext) => Promise | void; - - /** - * Called when the plugin is being deactivated. - */ - onDeactivate?: (context: PluginContext) => Promise | void; - - /** - * Called when the plugin is being unloaded or destroyed. - */ - onUnload?: (context: PluginContext) => Promise | void; - - /** - * Called to reload the plugin (without fully unloading it). - */ - onReload?: (config: PluginConfig, context: PluginContext) => Promise | void; -} - -/** - * Core Plugin Interface. - * All plugins must implement this interface to be loadable by the plugin loader. - */ -export interface PluginInterface extends PluginHooks { - /** Plugin metadata */ - metadata: PluginMetadata; - - /** Get the exported middleware (if this plugin exports middleware) */ - getMiddleware?(): NestMiddleware | ((req: Request, res: Response, next: NextFunction) => void | Promise); - - /** Get additional exports from the plugin */ - getExports?(): Record; - - /** Validate plugin configuration */ - validateConfig?(config: PluginConfig): { valid: boolean; errors: string[] }; - - /** Get plugin dependencies (list of required plugins) */ - getDependencies?(): string[]; - - /** Custom method for plugin-specific operations */ - [key: string]: any; -} - -/** - * Plugin Package definition (from package.json). - */ -export interface PluginPackageJson { - name: string; - version: string; - description?: string; - author?: string | { name?: string; email?: string; url?: string }; - homepage?: string; - repository?: - | string - | { - type?: string; - url?: string; - directory?: string; - }; - license?: string; - keywords?: string[]; - main?: string; - types?: string; - // Plugin-specific fields - mindblockPlugin?: { - version?: VersionConstraint; - priority?: number; - autoLoad?: boolean; - configSchema?: Record; - [key: string]: any; - }; - [key: string]: any; -} - -/** - * Represents a loaded plugin instance. - */ -export interface LoadedPlugin { - /** Plugin ID */ - id: string; - - /** Plugin metadata */ - metadata: PluginMetadata; - - /** Actual plugin instance */ - instance: PluginInterface; - - /** Plugin configuration */ - config: PluginConfig; - - /** Whether the plugin is currently active */ - active: boolean; - - /** Timestamp when plugin was loaded */ - loadedAt: Date; - - /** Plugin dependencies metadata */ - dependencies: string[]; -} - -/** - * Plugin search/filter criteria. - */ -export interface PluginSearchCriteria { - /** Search by plugin ID or name */ - query?: string; - - /** Filter by plugin keywords */ - keywords?: string[]; - - /** Filter by author */ - author?: string; - - /** Filter by enabled status */ - enabled?: boolean; - - /** Filter by active status */ - active?: boolean; - - /** Filter by priority range */ - priority?: { min?: number; max?: number }; -} - -/** - * Plugin validation result. - */ -export interface PluginValidationResult { - /** Whether validation passed */ - valid: boolean; - - /** Error messages if validation failed */ - errors: string[]; - - /** Warning messages */ - warnings: string[]; - - /** Additional metadata about validation */ - metadata?: Record; -} diff --git a/middleware/src/common/utils/index.ts b/middleware/src/common/utils/index.ts deleted file mode 100644 index 7a8b51fe..00000000 --- a/middleware/src/common/utils/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Plugin system exports -export * from './plugin-loader'; -export * from './plugin-registry'; -export * from '../interfaces/plugin.interface'; -export * from '../interfaces/plugin.errors'; diff --git a/middleware/src/common/utils/plugin-loader.ts b/middleware/src/common/utils/plugin-loader.ts deleted file mode 100644 index 3ba20a4d..00000000 --- a/middleware/src/common/utils/plugin-loader.ts +++ /dev/null @@ -1,628 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import * as path from 'path'; -import * as fs from 'fs'; -import { execSync } from 'child_process'; -import * as semver from 'semver'; - -import { - PluginInterface, - PluginMetadata, - PluginConfig, - PluginContext, - LoadedPlugin, - PluginPackageJson, - PluginValidationResult, - PluginSearchCriteria -} from '../interfaces/plugin.interface'; -import { - PluginLoadError, - PluginNotFoundError, - PluginAlreadyLoadedError, - PluginConfigError, - PluginDependencyError, - PluginVersionError, - PluginInitError, - PluginResolutionError, - InvalidPluginPackageError -} from '../interfaces/plugin.errors'; - -/** - * Plugin Loader Configuration - */ -export interface PluginLoaderConfig { - /** Directories to search for plugins (node_modules by default) */ - searchPaths?: string[]; - - /** Plugin name prefix to identify plugins (e.g., "@mindblock/plugin-") */ - pluginNamePrefix?: string; - - /** Middleware package version for compatibility checks */ - middlewareVersion?: string; - - /** Whether to auto-load plugins marked with autoLoad: true */ - autoLoadEnabled?: boolean; - - /** Maximum number of plugins to load */ - maxPlugins?: number; - - /** Whether to validate plugins strictly */ - strictMode?: boolean; - - /** Custom logger instance */ - logger?: Logger; -} - -/** - * Plugin Loader Service - * - * Responsible for: - * - Discovering npm packages that contain middleware plugins - * - Loading and instantiating plugins - * - Managing plugin lifecycle (load, init, activate, deactivate, unload) - * - Validating plugin configuration and dependencies - * - Providing plugin registry and search capabilities - */ -@Injectable() -export class PluginLoader { - private readonly logger: Logger; - private readonly searchPaths: string[]; - private readonly pluginNamePrefix: string; - private readonly middlewareVersion: string; - private readonly autoLoadEnabled: boolean; - private readonly maxPlugins: number; - private readonly strictMode: boolean; - - private loadedPlugins: Map = new Map(); - private pluginContext: PluginContext; - - constructor(config: PluginLoaderConfig = {}) { - this.logger = config.logger || new Logger('PluginLoader'); - this.searchPaths = config.searchPaths || this.getDefaultSearchPaths(); - this.pluginNamePrefix = config.pluginNamePrefix || '@mindblock/plugin-'; - this.middlewareVersion = config.middlewareVersion || '1.0.0'; - this.autoLoadEnabled = config.autoLoadEnabled !== false; - this.maxPlugins = config.maxPlugins || 100; - this.strictMode = config.strictMode !== false; - - this.pluginContext = { - logger: this.logger, - env: process.env, - plugins: this.loadedPlugins, - config: {} - }; - } - - /** - * Get default search paths for plugins - */ - private getDefaultSearchPaths(): string[] { - const nodeModulesPath = this.resolveNodeModulesPath(); - return [nodeModulesPath]; - } - - /** - * Resolve the node_modules path - */ - private resolveNodeModulesPath(): string { - try { - const nodeModulesPath = require.resolve('npm').split('node_modules')[0] + 'node_modules'; - if (fs.existsSync(nodeModulesPath)) { - return nodeModulesPath; - } - } catch (error) { - // Fallback - } - - // Fallback to relative path - return path.resolve(process.cwd(), 'node_modules'); - } - - /** - * Discover all available plugins in search paths - */ - async discoverPlugins(): Promise { - const discoveredPlugins: Map = new Map(); - - for (const searchPath of this.searchPaths) { - if (!fs.existsSync(searchPath)) { - this.logger.warn(`Search path does not exist: ${searchPath}`); - continue; - } - - try { - const entries = fs.readdirSync(searchPath); - - for (const entry of entries) { - // Check for scoped packages (@organization/plugin-name) - if (entry.startsWith('@')) { - const scopedPath = path.join(searchPath, entry); - if (!fs.statSync(scopedPath).isDirectory()) continue; - - const scopedEntries = fs.readdirSync(scopedPath); - for (const scopedEntry of scopedEntries) { - if (this.isPluginPackage(scopedEntry)) { - const pluginPackageJson = this.loadPluginPackageJson( - path.join(scopedPath, scopedEntry) - ); - if (pluginPackageJson) { - discoveredPlugins.set(pluginPackageJson.name, pluginPackageJson); - } - } - } - } else if (this.isPluginPackage(entry)) { - const pluginPackageJson = this.loadPluginPackageJson(path.join(searchPath, entry)); - if (pluginPackageJson) { - discoveredPlugins.set(pluginPackageJson.name, pluginPackageJson); - } - } - } - } catch (error) { - this.logger.error(`Error discovering plugins in ${searchPath}:`, error.message); - } - } - - return Array.from(discoveredPlugins.values()); - } - - /** - * Check if a package is a valid plugin package - */ - private isPluginPackage(packageName: string): boolean { - // Check if it starts with the plugin prefix - if (!packageName.includes('plugin-') && !packageName.startsWith('@mindblock/')) { - return false; - } - return packageName.includes('plugin-'); - } - - /** - * Load plugin package.json - */ - private loadPluginPackageJson(pluginPath: string): PluginPackageJson | null { - try { - const packageJsonPath = path.join(pluginPath, 'package.json'); - if (!fs.existsSync(packageJsonPath)) { - return null; - } - - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - - // Validate that it has plugin configuration - if (!packageJson.mindblockPlugin && !packageJson.main) { - return null; - } - - return packageJson; - } catch (error) { - this.logger.debug(`Failed to load package.json from ${pluginPath}:`, error.message); - return null; - } - } - - /** - * Load a plugin from an npm package - */ - async loadPlugin(pluginName: string, config?: PluginConfig): Promise { - // Check if already loaded - if (this.loadedPlugins.has(pluginName)) { - throw new PluginAlreadyLoadedError(pluginName); - } - - // Check plugin limit - if (this.loadedPlugins.size >= this.maxPlugins) { - throw new PluginLoadError(pluginName, `Maximum plugin limit (${this.maxPlugins}) reached`); - } - - try { - // Resolve plugin module - const pluginModule = await this.resolvePluginModule(pluginName); - if (!pluginModule) { - throw new PluginResolutionError(pluginName, 'Module not found'); - } - - // Load plugin instance - const pluginInstance = this.instantiatePlugin(pluginModule); - - // Validate plugin interface - this.validatePluginInterface(pluginInstance); - - // Get metadata - const metadata = pluginInstance.metadata; - - // Validate version compatibility - if (metadata.requiredMiddlewareVersion) { - this.validateVersionCompatibility(pluginName, metadata.requiredMiddlewareVersion); - } - - // Check dependencies - const dependencies = pluginInstance.getDependencies?.() || []; - this.validateDependencies(pluginName, dependencies); - - // Validate configuration - const pluginConfig = config || { enabled: true }; - if (pluginInstance.validateConfig) { - const validationResult = pluginInstance.validateConfig(pluginConfig); - if (!validationResult.valid) { - throw new PluginConfigError(pluginName, validationResult.errors); - } - } - - // Call onLoad hook - if (pluginInstance.onLoad) { - await pluginInstance.onLoad(this.pluginContext); - } - - // Create loaded plugin entry - const loadedPlugin: LoadedPlugin = { - id: metadata.id, - metadata, - instance: pluginInstance, - config: pluginConfig, - active: false, - loadedAt: new Date(), - dependencies - }; - - // Store loaded plugin - this.loadedPlugins.set(metadata.id, loadedPlugin); - - this.logger.log(`โœ“ Plugin loaded: ${metadata.id} (v${metadata.version})`); - - return loadedPlugin; - } catch (error) { - if (error instanceof PluginLoadError || error instanceof PluginConfigError || - error instanceof PluginDependencyError || error instanceof PluginResolutionError) { - throw error; - } - throw new PluginLoadError(pluginName, error.message, error); - } - } - - /** - * Resolve plugin module from npm package - */ - private async resolvePluginModule(pluginName: string): Promise { - try { - // Try direct require - return require(pluginName); - } catch (error) { - try { - // Try from node_modules - for (const searchPath of this.searchPaths) { - const pluginPath = path.join(searchPath, pluginName); - if (fs.existsSync(pluginPath)) { - const packageJsonPath = path.join(pluginPath, 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - const main = packageJson.main || 'index.js'; - const mainPath = path.join(pluginPath, main); - - if (fs.existsSync(mainPath)) { - return require(mainPath); - } - } - } - - throw new Error(`Plugin module not found in any search path`); - } catch (innerError) { - throw new PluginResolutionError(pluginName, innerError.message); - } - } - } - - /** - * Instantiate plugin from module - */ - private instantiatePlugin(pluginModule: any): PluginInterface { - // Check if it's a class or instance - if (pluginModule.default) { - return new pluginModule.default(); - } else if (typeof pluginModule === 'function') { - return new pluginModule(); - } else if (typeof pluginModule === 'object' && pluginModule.metadata) { - return pluginModule; - } - - throw new PluginLoadError('Unknown', 'Plugin module must export a class, function, or object with metadata'); - } - - /** - * Validate plugin interface - */ - private validatePluginInterface(plugin: any): void { - const errors: string[] = []; - - // Check metadata - if (!plugin.metadata) { - errors.push('Missing required property: metadata'); - } else { - const metadata = plugin.metadata; - if (!metadata.id) errors.push('Missing required metadata.id'); - if (!metadata.name) errors.push('Missing required metadata.name'); - if (!metadata.version) errors.push('Missing required metadata.version'); - if (!metadata.description) errors.push('Missing required metadata.description'); - } - - if (errors.length > 0) { - throw new InvalidPluginPackageError('', errors); - } - } - - /** - * Validate version compatibility - */ - private validateVersionCompatibility(pluginId: string, requiredVersion: string): void { - if (!semver.satisfies(this.middlewareVersion, requiredVersion)) { - throw new PluginVersionError( - pluginId, - requiredVersion, - this.middlewareVersion - ); - } - } - - /** - * Validate plugin dependencies - */ - private validateDependencies(pluginId: string, dependencies: string[]): void { - const missingDeps = dependencies.filter(dep => !this.loadedPlugins.has(dep)); - - if (missingDeps.length > 0) { - if (this.strictMode) { - throw new PluginDependencyError(pluginId, missingDeps); - } else { - this.logger.warn(`Plugin ${pluginId} has unmet dependencies:`, missingDeps.join(', ')); - } - } - } - - /** - * Initialize a loaded plugin - */ - async initPlugin(pluginId: string, config?: PluginConfig): Promise { - const loadedPlugin = this.loadedPlugins.get(pluginId); - if (!loadedPlugin) { - throw new PluginNotFoundError(pluginId); - } - - try { - const mergedConfig = { ...loadedPlugin.config, ...config }; - - // Call onInit hook - if (loadedPlugin.instance.onInit) { - await loadedPlugin.instance.onInit(mergedConfig, this.pluginContext); - } - - loadedPlugin.config = mergedConfig; - this.logger.log(`โœ“ Plugin initialized: ${pluginId}`); - } catch (error) { - throw new PluginInitError(pluginId, error.message, error); - } - } - - /** - * Activate a loaded plugin - */ - async activatePlugin(pluginId: string): Promise { - const loadedPlugin = this.loadedPlugins.get(pluginId); - if (!loadedPlugin) { - throw new PluginNotFoundError(pluginId); - } - - try { - // Call onActivate hook - if (loadedPlugin.instance.onActivate) { - await loadedPlugin.instance.onActivate(this.pluginContext); - } - - loadedPlugin.active = true; - this.logger.log(`โœ“ Plugin activated: ${pluginId}`); - } catch (error) { - throw new PluginInitError(pluginId, `Activation failed: ${error.message}`, error); - } - } - - /** - * Deactivate a plugin - */ - async deactivatePlugin(pluginId: string): Promise { - const loadedPlugin = this.loadedPlugins.get(pluginId); - if (!loadedPlugin) { - throw new PluginNotFoundError(pluginId); - } - - try { - // Call onDeactivate hook - if (loadedPlugin.instance.onDeactivate) { - await loadedPlugin.instance.onDeactivate(this.pluginContext); - } - - loadedPlugin.active = false; - this.logger.log(`โœ“ Plugin deactivated: ${pluginId}`); - } catch (error) { - this.logger.error(`Error deactivating plugin ${pluginId}:`, error.message); - } - } - - /** - * Unload a plugin - */ - async unloadPlugin(pluginId: string): Promise { - const loadedPlugin = this.loadedPlugins.get(pluginId); - if (!loadedPlugin) { - throw new PluginNotFoundError(pluginId); - } - - try { - // Deactivate first if active - if (loadedPlugin.active) { - await this.deactivatePlugin(pluginId); - } - - // Call onUnload hook - if (loadedPlugin.instance.onUnload) { - await loadedPlugin.instance.onUnload(this.pluginContext); - } - - this.loadedPlugins.delete(pluginId); - this.logger.log(`โœ“ Plugin unloaded: ${pluginId}`); - } catch (error) { - this.logger.error(`Error unloading plugin ${pluginId}:`, error.message); - } - } - - /** - * Reload a plugin (update config without full unload) - */ - async reloadPlugin(pluginId: string, config?: PluginConfig): Promise { - const loadedPlugin = this.loadedPlugins.get(pluginId); - if (!loadedPlugin) { - throw new PluginNotFoundError(pluginId); - } - - try { - const mergedConfig = { ...loadedPlugin.config, ...config }; - - // Call onReload hook - if (loadedPlugin.instance.onReload) { - await loadedPlugin.instance.onReload(mergedConfig, this.pluginContext); - } else { - // Fallback to deactivate + reactivate - if (loadedPlugin.active) { - await this.deactivatePlugin(pluginId); - } - loadedPlugin.config = mergedConfig; - await this.activatePlugin(pluginId); - } - - loadedPlugin.config = mergedConfig; - this.logger.log(`โœ“ Plugin reloaded: ${pluginId}`); - } catch (error) { - throw new PluginInitError(pluginId, `Reload failed: ${error.message}`, error); - } - } - - /** - * Get a loaded plugin by ID - */ - getPlugin(pluginId: string): LoadedPlugin | undefined { - return this.loadedPlugins.get(pluginId); - } - - /** - * Get all loaded plugins - */ - getAllPlugins(): LoadedPlugin[] { - return Array.from(this.loadedPlugins.values()); - } - - /** - * Get active plugins only - */ - getActivePlugins(): LoadedPlugin[] { - return this.getAllPlugins().filter(p => p.active); - } - - /** - * Search plugins by criteria - */ - searchPlugins(criteria: PluginSearchCriteria): LoadedPlugin[] { - let results = this.getAllPlugins(); - - if (criteria.query) { - const query = criteria.query.toLowerCase(); - results = results.filter( - p => p.metadata.id.toLowerCase().includes(query) || - p.metadata.name.toLowerCase().includes(query) - ); - } - - if (criteria.keywords && criteria.keywords.length > 0) { - results = results.filter( - p => p.metadata.keywords && - criteria.keywords.some(kw => p.metadata.keywords.includes(kw)) - ); - } - - if (criteria.author) { - results = results.filter(p => p.metadata.author?.toLowerCase() === criteria.author.toLowerCase()); - } - - if (criteria.enabled !== undefined) { - results = results.filter(p => (p.config.enabled ?? true) === criteria.enabled); - } - - if (criteria.active !== undefined) { - results = results.filter(p => p.active === criteria.active); - } - - if (criteria.priority) { - results = results.filter(p => { - const priority = p.metadata.priority ?? 0; - if (criteria.priority.min !== undefined && priority < criteria.priority.min) return false; - if (criteria.priority.max !== undefined && priority > criteria.priority.max) return false; - return true; - }); - } - - return results.sort((a, b) => (b.metadata.priority ?? 0) - (a.metadata.priority ?? 0)); - } - - /** - * Validate plugin configuration - */ - validatePluginConfig(pluginId: string, config: PluginConfig): PluginValidationResult { - const plugin = this.loadedPlugins.get(pluginId); - if (!plugin) { - return { - valid: false, - errors: [`Plugin not found: ${pluginId}`], - warnings: [] - }; - } - - const errors: string[] = []; - const warnings: string[] = []; - - // Validate using plugin's validator if available - if (plugin.instance.validateConfig) { - const result = plugin.instance.validateConfig(config); - errors.push(...result.errors); - } - - // Check if disabled plugins should not be configured - if (config.enabled === false && config.options) { - warnings.push('Plugin is disabled but options are provided'); - } - - return { - valid: errors.length === 0, - errors, - warnings - }; - } - - /** - * Get plugin statistics - */ - getStatistics(): { - totalLoaded: number; - totalActive: number; - totalDisabled: number; - plugins: Array<{ id: string; name: string; version: string; active: boolean; priority: number }>; - } { - const plugins = this.getAllPlugins().sort((a, b) => (b.metadata.priority ?? 0) - (a.metadata.priority ?? 0)); - - return { - totalLoaded: plugins.length, - totalActive: plugins.filter(p => p.active).length, - totalDisabled: plugins.filter(p => !p.config.enabled).length, - plugins: plugins.map(p => ({ - id: p.metadata.id, - name: p.metadata.name, - version: p.metadata.version, - active: p.active, - priority: p.metadata.priority ?? 0 - })) - }; - } -} diff --git a/middleware/src/common/utils/plugin-registry.ts b/middleware/src/common/utils/plugin-registry.ts deleted file mode 100644 index d60dea9b..00000000 --- a/middleware/src/common/utils/plugin-registry.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { PluginLoader, PluginLoaderConfig } from './plugin-loader'; -import { - PluginInterface, - PluginConfig, - LoadedPlugin, - PluginSearchCriteria, - PluginValidationResult -} from '../interfaces/plugin.interface'; -import { PluginNotFoundError, PluginLoadError } from '../interfaces/plugin.errors'; - -/** - * Plugin Registry Configuration - */ -export interface PluginRegistryConfig extends PluginLoaderConfig { - /** Automatically discover and load plugins on initialization */ - autoDiscoverOnInit?: boolean; - - /** Plugins to load automatically */ - autoLoadPlugins?: string[]; - - /** Default configuration for all plugins */ - defaultConfig?: PluginConfig; -} - -/** - * Plugin Registry - * - * High-level service for managing plugins. Provides: - * - Plugin discovery and loading - * - Lifecycle management - * - Plugin registry operations - * - Middleware integration - */ -@Injectable() -export class PluginRegistry { - private readonly logger: Logger; - private readonly loader: PluginLoader; - private readonly autoDiscoverOnInit: boolean; - private readonly autoLoadPlugins: string[]; - private readonly defaultConfig: PluginConfig; - private initialized: boolean = false; - - constructor(config: PluginRegistryConfig = {}) { - this.logger = config.logger || new Logger('PluginRegistry'); - this.loader = new PluginLoader(config); - this.autoDiscoverOnInit = config.autoDiscoverOnInit !== false; - this.autoLoadPlugins = config.autoLoadPlugins || []; - this.defaultConfig = config.defaultConfig || { enabled: true }; - } - - /** - * Initialize the plugin registry - * - Discover available plugins - * - Load auto-load plugins - */ - async init(): Promise { - if (this.initialized) { - this.logger.warn('Plugin registry already initialized'); - return; - } - - try { - this.logger.log('๐Ÿ”Œ Initializing Plugin Registry...'); - - // Discover available plugins - if (this.autoDiscoverOnInit) { - this.logger.log('๐Ÿ“ฆ Discovering available plugins...'); - const discovered = await this.loader.discoverPlugins(); - this.logger.log(`โœ“ Found ${discovered.length} available plugins`); - } - - // Auto-load configured plugins - if (this.autoLoadPlugins.length > 0) { - this.logger.log(`๐Ÿ“ฅ Auto-loading ${this.autoLoadPlugins.length} plugins...`); - for (const pluginName of this.autoLoadPlugins) { - try { - await this.load(pluginName); - } catch (error) { - this.logger.warn(`Failed to auto-load plugin ${pluginName}: ${error.message}`); - } - } - } - - this.initialized = true; - const stats = this.getStatistics(); - this.logger.log(`โœ“ Plugin Registry initialized - ${stats.totalLoaded} plugins loaded, ${stats.totalActive} active`); - } catch (error) { - this.logger.error('Failed to initialize Plugin Registry:', error.message); - throw error; - } - } - - /** - * Load a plugin - */ - async load(pluginName: string, config?: PluginConfig): Promise { - const mergedConfig = { ...this.defaultConfig, ...config }; - return this.loader.loadPlugin(pluginName, mergedConfig); - } - - /** - * Initialize a plugin (setup with configuration) - */ - async initialize(pluginId: string, config?: PluginConfig): Promise { - return this.loader.initPlugin(pluginId, config); - } - - /** - * Activate a plugin - */ - async activate(pluginId: string): Promise { - return this.loader.activatePlugin(pluginId); - } - - /** - * Deactivate a plugin - */ - async deactivate(pluginId: string): Promise { - return this.loader.deactivatePlugin(pluginId); - } - - /** - * Unload a plugin - */ - async unload(pluginId: string): Promise { - return this.loader.unloadPlugin(pluginId); - } - - /** - * Reload a plugin with new configuration - */ - async reload(pluginId: string, config?: PluginConfig): Promise { - return this.loader.reloadPlugin(pluginId, config); - } - - /** - * Load and activate a plugin in one step - */ - async loadAndActivate(pluginName: string, config?: PluginConfig): Promise { - const loaded = await this.load(pluginName, config); - await this.initialize(loaded.metadata.id, config); - await this.activate(loaded.metadata.id); - return loaded; - } - - /** - * Get plugin by ID - */ - getPlugin(pluginId: string): LoadedPlugin | undefined { - return this.loader.getPlugin(pluginId); - } - - /** - * Get plugin by ID or throw error - */ - getPluginOrThrow(pluginId: string): LoadedPlugin { - const plugin = this.getPlugin(pluginId); - if (!plugin) { - throw new PluginNotFoundError(pluginId); - } - return plugin; - } - - /** - * Get all plugins - */ - getAllPlugins(): LoadedPlugin[] { - return this.loader.getAllPlugins(); - } - - /** - * Get active plugins only - */ - getActivePlugins(): LoadedPlugin[] { - return this.loader.getActivePlugins(); - } - - /** - * Search plugins - */ - searchPlugins(criteria: PluginSearchCriteria): LoadedPlugin[] { - return this.loader.searchPlugins(criteria); - } - - /** - * Validate plugin configuration - */ - validateConfig(pluginId: string, config: PluginConfig): PluginValidationResult { - return this.loader.validatePluginConfig(pluginId, config); - } - - /** - * Get plugin middleware - */ - getMiddleware(pluginId: string) { - const plugin = this.getPluginOrThrow(pluginId); - - if (!plugin.instance.getMiddleware) { - throw new PluginLoadError( - pluginId, - 'Plugin does not export middleware' - ); - } - - return plugin.instance.getMiddleware(); - } - - /** - * Get all plugin middlewares - */ - getAllMiddleware() { - const middlewares: Record = {}; - - for (const plugin of this.getActivePlugins()) { - if (plugin.instance.getMiddleware && plugin.config.enabled !== false) { - middlewares[plugin.metadata.id] = plugin.instance.getMiddleware(); - } - } - - return middlewares; - } - - /** - * Get plugin exports - */ - getExports(pluginId: string): Record | undefined { - const plugin = this.getPluginOrThrow(pluginId); - return plugin.instance.getExports?.(); - } - - /** - * Get all plugin exports - */ - getAllExports(): Record { - const allExports: Record = {}; - - for (const plugin of this.getAllPlugins()) { - if (plugin.instance.getExports) { - const exports = plugin.instance.getExports(); - if (exports) { - allExports[plugin.metadata.id] = exports; - } - } - } - - return allExports; - } - - /** - * Check if plugin is loaded - */ - isLoaded(pluginId: string): boolean { - return this.loader.getPlugin(pluginId) !== undefined; - } - - /** - * Check if plugin is active - */ - isActive(pluginId: string): boolean { - const plugin = this.loader.getPlugin(pluginId); - return plugin?.active ?? false; - } - - /** - * Count plugins - */ - count(): number { - return this.getAllPlugins().length; - } - - /** - * Count active plugins - */ - countActive(): number { - return this.getActivePlugins().length; - } - - /** - * Get registry statistics - */ - getStatistics() { - return this.loader.getStatistics(); - } - - /** - * Unload all plugins - */ - async unloadAll(): Promise { - const plugins = [...this.getAllPlugins()]; - - for (const plugin of plugins) { - try { - await this.unload(plugin.metadata.id); - } catch (error) { - this.logger.error(`Error unloading plugin ${plugin.metadata.id}:`, error.message); - } - } - - this.logger.log('โœ“ All plugins unloaded'); - } - - /** - * Activate all enabled plugins - */ - async activateAll(): Promise { - for (const plugin of this.getAllPlugins()) { - if (plugin.config.enabled !== false && !plugin.active) { - try { - await this.activate(plugin.metadata.id); - } catch (error) { - this.logger.error(`Error activating plugin ${plugin.metadata.id}:`, error.message); - } - } - } - } - - /** - * Deactivate all plugins - */ - async deactivateAll(): Promise { - for (const plugin of this.getActivePlugins()) { - try { - await this.deactivate(plugin.metadata.id); - } catch (error) { - this.logger.error(`Error deactivating plugin ${plugin.metadata.id}:`, error.message); - } - } - } - - /** - * Export registry state (for debugging/monitoring) - */ - exportState(): { - initialized: boolean; - totalPlugins: number; - activePlugins: number; - plugins: Array<{ - id: string; - name: string; - version: string; - active: boolean; - enabled: boolean; - priority: number; - dependencies: string[]; - }>; - } { - return { - initialized: this.initialized, - totalPlugins: this.count(), - activePlugins: this.countActive(), - plugins: this.getAllPlugins().map(p => ({ - id: p.metadata.id, - name: p.metadata.name, - version: p.metadata.version, - active: p.active, - enabled: p.config.enabled !== false, - priority: p.metadata.priority ?? 0, - dependencies: p.dependencies - })) - }; - } - - /** - * Check initialization status - */ - isInitialized(): boolean { - return this.initialized; - } -} diff --git a/middleware/src/index.ts b/middleware/src/index.ts index e28b0371..088f941a 100644 --- a/middleware/src/index.ts +++ b/middleware/src/index.ts @@ -18,9 +18,3 @@ export * from './middleware/advanced/circuit-breaker.middleware'; // Blockchain module โ€” Issues #307, #308, #309, #310 export * from './blockchain'; - -// External Plugin Loader System -export * from './common/utils/plugin-loader'; -export * from './common/utils/plugin-registry'; -export * from './common/interfaces/plugin.interface'; -export * from './common/interfaces/plugin.errors'; diff --git a/middleware/src/plugins/example.plugin.ts b/middleware/src/plugins/example.plugin.ts deleted file mode 100644 index 0e5937ad..00000000 --- a/middleware/src/plugins/example.plugin.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { NestMiddleware, Logger } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { - PluginInterface, - PluginMetadata, - PluginConfig, - PluginContext -} from '../common/interfaces/plugin.interface'; - -/** - * Example Plugin Template - * - * This is a template for creating custom middleware plugins for the @mindblock/middleware package. - * - * Usage: - * 1. Copy this file to your plugin project - * 2. Implement the required methods (getMiddleware, etc.) - * 3. Export an instance or class from your plugin's main entry point - * 4. Add plugin configuration to your package.json - */ -export class ExamplePlugin implements PluginInterface { - private readonly logger = new Logger('ExamplePlugin'); - private isInitialized = false; - - // Required: Plugin metadata - metadata: PluginMetadata = { - id: 'com.example.plugin.demo', - name: 'Example Plugin', - description: 'A template example plugin for middleware', - version: '1.0.0', - author: 'Your Name/Organization', - homepage: 'https://github.com/your-org/plugin-example', - license: 'MIT', - keywords: ['example', 'template', 'middleware'], - priority: 10, - autoLoad: false - }; - - /** - * Optional: Called when plugin is first loaded - */ - async onLoad(context: PluginContext): Promise { - this.logger.log('Plugin loaded'); - // Perform initial setup: validate dependencies, check environment, etc. - } - - /** - * Optional: Called when plugin is initialized with configuration - */ - async onInit(config: PluginConfig, context: PluginContext): Promise { - this.logger.log('Plugin initialized with config:', config); - this.isInitialized = true; - // Initialize based on provided configuration - } - - /** - * Optional: Called when plugin is activated - */ - async onActivate(context: PluginContext): Promise { - this.logger.log('Plugin activated'); - // Perform activation tasks (start services, open connections, etc.) - } - - /** - * Optional: Called when plugin is deactivated - */ - async onDeactivate(context: PluginContext): Promise { - this.logger.log('Plugin deactivated'); - // Perform cleanup (stop services, close connections, etc.) - } - - /** - * Optional: Called when plugin is unloaded - */ - async onUnload(context: PluginContext): Promise { - this.logger.log('Plugin unloaded'); - // Final cleanup - } - - /** - * Optional: Called when plugin is reloaded - */ - async onReload(config: PluginConfig, context: PluginContext): Promise { - this.logger.log('Plugin reloaded with new config:', config); - await this.onDeactivate(context); - await this.onInit(config, context); - await this.onActivate(context); - } - - /** - * Optional: Validate provided configuration - */ - validateConfig(config: PluginConfig): { valid: boolean; errors: string[] } { - const errors: string[] = []; - - if (config.options) { - // Add your validation logic here - if (config.options.someRequiredField === undefined) { - errors.push('someRequiredField is required'); - } - } - - return { valid: errors.length === 0, errors }; - } - - /** - * Optional: Get list of plugin dependencies - */ - getDependencies(): string[] { - return []; // Return IDs of plugins that must be loaded before this one - } - - /** - * Export the middleware (if this plugin provides a middleware) - */ - getMiddleware(): NestMiddleware { - return { - use: (req: Request, res: Response, next: NextFunction) => { - this.logger.log(`Example middleware - ${req.method} ${req.path}`); - - // Your middleware logic here - // Example: add custom header - res.setHeader('X-Example-Plugin', 'active'); - - // Continue to next middleware - next(); - } - }; - } - - /** - * Optional: Export additional utilities/helpers from the plugin - */ - getExports(): Record { - return { - exampleFunction: () => 'Hello from example plugin', - exampleValue: 42 - }; - } - - /** - * Custom method example - */ - customMethod(data: string): string { - if (!this.isInitialized) { - throw new Error('Plugin not initialized'); - } - return `Processed: ${data}`; - } -} - -// Export as default for easier importing -export default ExamplePlugin; - -/** - * Plugin package.json configuration example: - * - * { - * "name": "@yourorg/plugin-example", - * "version": "1.0.0", - * "description": "Example middleware plugin", - * "main": "dist/example.plugin.js", - * "types": "dist/example.plugin.d.ts", - * "license": "MIT", - * "keywords": ["mindblock", "plugin", "middleware"], - * "mindblockPlugin": { - * "version": "^1.0.0", - * "priority": 10, - * "autoLoad": false, - * "configSchema": { - * "type": "object", - * "properties": { - * "enabled": { "type": "boolean", "default": true }, - * "options": { - * "type": "object", - * "properties": { - * "someRequiredField": { "type": "string" } - * } - * } - * } - * } - * }, - * "dependencies": { - * "@nestjs/common": "^11.0.0", - * "@mindblock/middleware": "^1.0.0" - * }, - * "devDependencies": { - * "@types/express": "^5.0.0", - * "@types/node": "^20.0.0", - * "typescript": "^5.0.0" - * } - * } - */ diff --git a/middleware/src/security/index.ts b/middleware/src/security/index.ts index c6f98f38..f3e26a5f 100644 --- a/middleware/src/security/index.ts +++ b/middleware/src/security/index.ts @@ -1,4 +1,3 @@ -// Security middleware exports +// Placeholder: security middleware exports will live here. -export * from './security-headers.middleware'; -export * from './security-headers.config'; +export const __securityPlaceholder = true; diff --git a/middleware/tests/integration/benchmark.integration.spec.ts b/middleware/tests/integration/benchmark.integration.spec.ts deleted file mode 100644 index 55a4e09f..00000000 --- a/middleware/tests/integration/benchmark.integration.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SecurityHeadersMiddleware } from '../../src/security/security-headers.middleware'; -import { TimeoutMiddleware } from '../../src/middleware/advanced/timeout.middleware'; -import { CircuitBreakerMiddleware, CircuitBreakerService } from '../../src/middleware/advanced/circuit-breaker.middleware'; -import { CorrelationIdMiddleware } from '../../src/monitoring/correlation-id.middleware'; - -describe('Middleware Benchmark Integration', () => { - it('should instantiate all benchmarked middleware without errors', () => { - // Test SecurityHeadersMiddleware - const securityMiddleware = new SecurityHeadersMiddleware(); - expect(securityMiddleware).toBeDefined(); - expect(typeof securityMiddleware.use).toBe('function'); - - // Test TimeoutMiddleware - const timeoutMiddleware = new TimeoutMiddleware({ timeout: 5000 }); - expect(timeoutMiddleware).toBeDefined(); - expect(typeof timeoutMiddleware.use).toBe('function'); - - // Test CircuitBreakerMiddleware - const circuitBreakerService = new CircuitBreakerService({ - failureThreshold: 5, - recoveryTimeout: 30000, - monitoringPeriod: 10000 - }); - const circuitBreakerMiddleware = new CircuitBreakerMiddleware(circuitBreakerService); - expect(circuitBreakerMiddleware).toBeDefined(); - expect(typeof circuitBreakerMiddleware.use).toBe('function'); - - // Test CorrelationIdMiddleware - const correlationMiddleware = new CorrelationIdMiddleware(); - expect(correlationMiddleware).toBeDefined(); - expect(typeof correlationMiddleware.use).toBe('function'); - }); - - it('should have all required middleware exports', () => { - // This test ensures the middleware are properly exported for benchmarking - expect(SecurityHeadersMiddleware).toBeDefined(); - expect(TimeoutMiddleware).toBeDefined(); - expect(CircuitBreakerMiddleware).toBeDefined(); - expect(CircuitBreakerService).toBeDefined(); - expect(CorrelationIdMiddleware).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/middleware/tests/integration/plugin-system.integration.spec.ts b/middleware/tests/integration/plugin-system.integration.spec.ts deleted file mode 100644 index d5ce3204..00000000 --- a/middleware/tests/integration/plugin-system.integration.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { PluginLoader } from '../../src/common/utils/plugin-loader'; -import { PluginRegistry } from '../../src/common/utils/plugin-registry'; -import { PluginInterface, PluginMetadata } from '../../src/common/interfaces/plugin.interface'; -import { - PluginNotFoundError, - PluginAlreadyLoadedError, - PluginConfigError, - PluginDependencyError -} from '../../src/common/interfaces/plugin.errors'; - -/** - * Mock Plugin for testing - */ -class MockPlugin implements PluginInterface { - metadata: PluginMetadata = { - id: 'test-plugin', - name: 'Test Plugin', - description: 'A test plugin', - version: '1.0.0' - }; - - async onLoad() { - // Test hook - } - - async onInit() { - // Test hook - } - - async onActivate() { - // Test hook - } - - validateConfig() { - return { valid: true, errors: [] }; - } - - getDependencies() { - return []; - } - - getMiddleware() { - return (req: any, res: any, next: any) => next(); - } - - getExports() { - return { testExport: 'value' }; - } -} - -/** - * Mock Plugin with Dependencies - */ -class MockPluginWithDeps implements PluginInterface { - metadata: PluginMetadata = { - id: 'test-plugin-deps', - name: 'Test Plugin With Deps', - description: 'A test plugin with dependencies', - version: '1.0.0' - }; - - getDependencies() { - return ['test-plugin']; - } -} - -describe('PluginLoader', () => { - let loader: PluginLoader; - let mockPlugin: MockPlugin; - - beforeEach(() => { - loader = new PluginLoader({ - logger: new Logger('Test'), - middlewareVersion: '1.0.0' - }); - mockPlugin = new MockPlugin(); - }); - - describe('loadPlugin', () => { - it('should load a valid plugin', async () => { - // Mock require to return our test plugin - const originalRequire = global.require; - (global as any).require = jest.fn((moduleId: string) => { - if (moduleId === 'test-plugin') { - return { default: MockPlugin }; - } - return originalRequire(moduleId); - }); - - // Note: In actual testing, we'd need to mock the module resolution - expect(mockPlugin.metadata.id).toBe('test-plugin'); - }); - - it('should reject duplicate plugin loads', async () => { - // This would require proper test setup with module mocking - }); - }); - - describe('plugin validation', () => { - it('should validate plugin interface', () => { - // Valid plugin metadata - expect(mockPlugin.metadata).toBeDefined(); - expect(mockPlugin.metadata.id).toBeDefined(); - expect(mockPlugin.metadata.name).toBeDefined(); - expect(mockPlugin.metadata.version).toBeDefined(); - }); - - it('should validate plugin configuration', () => { - const result = mockPlugin.validateConfig({ enabled: true }); - expect(result.valid).toBe(true); - expect(result.errors.length).toBe(0); - }); - }); - - describe('plugin lifecycle', () => { - it('should have all lifecycle hooks defined', async () => { - expect(typeof mockPlugin.onLoad).toBe('function'); - expect(typeof mockPlugin.onInit).toBe('function'); - expect(typeof mockPlugin.onActivate).toBe('function'); - expect(mockPlugin.validateConfig).toBeDefined(); - }); - - it('should execute hooks in order', async () => { - const hooks: string[] = []; - - const testPlugin: PluginInterface = { - metadata: mockPlugin.metadata, - onLoad: async () => hooks.push('onLoad'), - onInit: async () => hooks.push('onInit'), - onActivate: async () => hooks.push('onActivate'), - validateConfig: () => ({ valid: true, errors: [] }), - getDependencies: () => [] - }; - - await testPlugin.onLoad!({}); - await testPlugin.onInit!({}, {}); - await testPlugin.onActivate!({}); - - expect(hooks).toEqual(['onLoad', 'onInit', 'onActivate']); - }); - }); - - describe('plugin exports', () => { - it('should export middleware', () => { - const middleware = mockPlugin.getMiddleware(); - expect(middleware).toBeDefined(); - expect(typeof middleware).toBe('function'); - }); - - it('should export utilities', () => { - const exports = mockPlugin.getExports(); - expect(exports).toBeDefined(); - expect(exports.testExport).toBe('value'); - }); - }); - - describe('plugin dependencies', () => { - it('should return dependency list', () => { - const deps = mockPlugin.getDependencies(); - expect(Array.isArray(deps)).toBe(true); - - const depsPlugin = new MockPluginWithDeps(); - const depsPluginDeps = depsPlugin.getDependencies(); - expect(depsPluginDeps).toContain('test-plugin'); - }); - }); -}); - -describe('PluginRegistry', () => { - let registry: PluginRegistry; - - beforeEach(() => { - registry = new PluginRegistry({ - logger: new Logger('Test'), - middlewareVersion: '1.0.0' - }); - }); - - describe('initialization', () => { - it('should initialize registry', async () => { - // Note: In actual testing, we'd mock the loader - expect(registry.isInitialized()).toBe(false); - }); - }); - - describe('plugin management', () => { - it('should count plugins', () => { - expect(registry.count()).toBe(0); - }); - - it('should check if initialized', () => { - expect(registry.isInitialized()).toBe(false); - }); - - it('should export state', () => { - const state = registry.exportState(); - expect(state).toHaveProperty('initialized'); - expect(state).toHaveProperty('totalPlugins'); - expect(state).toHaveProperty('activePlugins'); - expect(state).toHaveProperty('plugins'); - expect(Array.isArray(state.plugins)).toBe(true); - }); - }); - - describe('plugin search', () => { - it('should search plugins with empty registry', () => { - const results = registry.searchPlugins({ query: 'test' }); - expect(Array.isArray(results)).toBe(true); - expect(results.length).toBe(0); - }); - }); - - describe('batch operations', () => { - it('should handle batch plugin operations', async () => { - // Test unloadAll - await expect(registry.unloadAll()).resolves.not.toThrow(); - - // Test activateAll - await expect(registry.activateAll()).resolves.not.toThrow(); - - // Test deactivateAll - await expect(registry.deactivateAll()).resolves.not.toThrow(); - }); - }); - - describe('statistics', () => { - it('should provide statistics', () => { - const stats = registry.getStatistics(); - expect(stats).toHaveProperty('totalLoaded', 0); - expect(stats).toHaveProperty('totalActive', 0); - expect(stats).toHaveProperty('totalDisabled', 0); - expect(Array.isArray(stats.plugins)).toBe(true); - }); - }); -}); - -describe('Plugin Errors', () => { - it('should create PluginNotFoundError', () => { - const error = new PluginNotFoundError('test-plugin'); - expect(error.message).toContain('test-plugin'); - expect(error.code).toBe('PLUGIN_NOT_FOUND'); - }); - - it('should create PluginAlreadyLoadedError', () => { - const error = new PluginAlreadyLoadedError('test-plugin'); - expect(error.message).toContain('test-plugin'); - expect(error.code).toBe('PLUGIN_ALREADY_LOADED'); - }); - - it('should create PluginConfigError', () => { - const error = new PluginConfigError('test-plugin', ['Invalid field']); - expect(error.message).toContain('test-plugin'); - expect(error.code).toBe('PLUGIN_CONFIG_ERROR'); - }); - - it('should create PluginDependencyError', () => { - const error = new PluginDependencyError('test-plugin', ['dep1', 'dep2']); - expect(error.message).toContain('dep1'); - expect(error.code).toBe('PLUGIN_DEPENDENCY_ERROR'); - }); -}); diff --git a/middleware/tsconfig.json b/middleware/tsconfig.json index 6feb2686..de7bda18 100644 --- a/middleware/tsconfig.json +++ b/middleware/tsconfig.json @@ -21,6 +21,6 @@ "@validation/*": ["src/validation/*"] } }, - "include": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"], + "include": ["src/**/*.ts", "tests/**/*.ts"], "exclude": ["node_modules", "dist", "coverage"] }