# @memberjunction/component-registry-server REST API server for serving MemberJunction interactive components. Provides a standardized API for discovering, searching, and retrieving components and their specifications. ## Overview The Component Registry Server provides a standardized REST API for discovering and retrieving MemberJunction interactive components. It implements the Component Registry API v1 specification, allowing any MemberJunction system to connect and consume components dynamically. ```mermaid graph TD A["Component Registry Server
(Express)"] --> B["GET /api/v1/registry"] A --> C["GET /api/v1/health"] A --> D["GET /api/v1/components"] A --> E["GET /api/v1/components/search"] A --> F["GET /api/v1/components/:ns/:name"] A --> G["MemberJunction Database"] G --> H["MJ: Components"] G --> I["MJ: Component Registries"] G --> J["MJ: Component Libraries"] K["Client SDK"] -->|HTTP| A L["MJ Explorer"] -->|HTTP| A style A fill:#2d6a9f,stroke:#1a4971,color:#fff style B fill:#2d8659,stroke:#1a5c3a,color:#fff style C fill:#2d8659,stroke:#1a5c3a,color:#fff style D fill:#2d8659,stroke:#1a5c3a,color:#fff style E fill:#2d8659,stroke:#1a5c3a,color:#fff style F fill:#2d8659,stroke:#1a5c3a,color:#fff style G fill:#7c5295,stroke:#563a6b,color:#fff style K fill:#b8762f,stroke:#8a5722,color:#fff style L fill:#b8762f,stroke:#8a5722,color:#fff ``` ## Features - **REST API v1**: Standardized endpoints for component discovery and retrieval - **Extensible Architecture**: Base class design allows easy customization via inheritance - **Authentication Support**: Override methods to implement custom authentication - **Database Integration**: Uses MemberJunction's entity system for component storage - **CORS Support**: Configurable cross-origin resource sharing - **Component Versioning**: Automatic selection of latest component versions - **Search Capabilities**: Full-text search across component names, titles, and descriptions ## Installation ```bash npm install @memberjunction/component-registry-server ``` ## Quick Start ### 1. Configure the Server Add configuration to your `mj.config.cjs` file in the project root: ```javascript module.exports = { // Database configuration dbHost: 'localhost', dbDatabase: 'MemberJunction', dbUsername: 'your-username', dbPassword: 'your-password', mjCoreSchema: '__mj', // Component Registry configuration componentRegistrySettings: { port: 3200, // Port to run the server on enableRegistry: true, // Enable the registry server registryId: null, // Optional: GUID of registry record requireAuth: false, // Whether to require authentication corsOrigins: ['*'] // Allowed CORS origins } }; ``` ### 2. Run the Server ```bash # Using npm scripts npm start # Or directly via node node dist/index.js ``` The server will start on the configured port (default: 3200) and be available at: ``` http://localhost:3200/api/v1 ``` ## API Endpoints ### Registry Information **GET /api/v1/registry** ```json { "name": "Local Component Registry", "description": "MemberJunction Component Registry", "version": "v1", "requiresAuth": false } ``` ### Health Check **GET /api/v1/health** ```json { "status": "healthy", "timestamp": "2024-01-01T00:00:00.000Z", "version": "v1", "componentCount": 42 } ``` ### List Components **GET /api/v1/components** ```json { "components": [ { "namespace": "@memberjunction/dashboards", "name": "revenue-tracker", "version": "1.0.0", "title": "Revenue Tracker Dashboard", "description": "Track revenue metrics over time", "type": "Dashboard", "status": "Published" } ], "total": 1 } ``` ### Search Components **GET /api/v1/components/search?q=revenue&type=Dashboard** ```json { "results": [...], "total": 5, "query": "revenue" } ``` ### Get Component **GET /api/v1/components/:namespace/:name** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "namespace": "@memberjunction/dashboards", "name": "revenue-tracker", "version": "1.0.0", "specification": { "name": "revenue-tracker", "title": "Revenue Tracker Dashboard", "code": "// Component code here", "props": [...], "dataRequirements": [...] } } ``` Optional version parameter: **GET /api/v1/components/:namespace/:name?version=1.0.0** ## Extending the Server The Component Registry Server is designed to be easily extended through inheritance. The base `ComponentRegistryAPIServer` class provides virtual methods that can be overridden to customize behavior. ### Custom Authentication Create a subclass and override the `checkAPIKey` method: ```typescript import { ComponentRegistryAPIServer } from '@memberjunction/component-registry-server'; import { Request } from 'express'; class MyAuthenticatedRegistryServer extends ComponentRegistryAPIServer { /** * Implement custom authentication logic */ protected async checkAPIKey(req: Request): Promise { // Check for API key in header const apiKey = req.headers['x-api-key'] as string; if (!apiKey) { return false; } // Validate against your auth provider return await this.validateWithAuthProvider(apiKey); } private async validateWithAuthProvider(apiKey: string): Promise { // Your custom validation logic here // Could check database, external service, JWT, etc. return apiKey === process.env.VALID_API_KEY; } } // Start your custom server const server = new MyAuthenticatedRegistryServer(); await server.initialize(); await server.start(); ``` ### Custom Component Filtering Override the `getComponentFilter` method to control which components are served: ```typescript class FilteredRegistryServer extends ComponentRegistryAPIServer { /** * Only serve components from specific registries */ protected getComponentFilter(): string { // Serve local components and components from a specific registry return "(SourceRegistryID IS NULL OR SourceRegistryID = 'abc-123-def')"; } } ``` ### Using Different Database Tables Override the route handlers to pull components from different tables or entities: ```typescript class CustomTableRegistryServer extends ComponentRegistryAPIServer { /** * Use a different entity/table for components * For example, using a "Custom Components" entity instead of "MJ: Components" */ protected async listComponents(req: Request, res: Response): Promise { try { const rv = new RunView(); const result = await rv.RunView({ EntityName: 'Custom Components', // Your custom entity name ExtraFilter: "IsActive = 1 AND IsPublished = 1", OrderBy: 'CategoryName, ComponentName, Version DESC', ResultType: 'entity_object' }); if (!result.Success) { res.status(500).json({ error: result.ErrorMessage }); return; } // Map your custom fields to the API response format const components = (result.Results || []).map(c => ({ namespace: c.CategoryName, // Map your fields name: c.ComponentName, // to the expected version: c.Version, // API format title: c.DisplayName, description: c.Summary, type: c.ComponentType, status: c.PublishStatus })); res.json({ components, total: components.length }); } catch (error) { res.status(500).json({ error: 'Failed to list components' }); } } protected async getComponent(req: Request, res: Response): Promise { const { namespace, name } = req.params; const rv = new RunView(); const result = await rv.RunView({ EntityName: 'Custom Components', ExtraFilter: `CategoryName = '${namespace}' AND ComponentName = '${name}'`, OrderBy: 'Version DESC', MaxRows: 1, ResultType: 'entity_object' }); if (!result.Success || !result.Results?.length) { res.status(404).json({ error: 'Component not found' }); return; } const component = result.Results[0]; res.json({ id: component.ID, namespace: component.CategoryName, name: component.ComponentName, version: component.Version, specification: JSON.parse(component.ComponentSpec) // Your spec field }); } // Override search to use your custom table protected async searchComponents(req: Request, res: Response): Promise { const { q, type } = req.query; let filter = "IsActive = 1 AND IsPublished = 1"; if (q) { filter += ` AND (ComponentName LIKE '%${q}%' OR DisplayName LIKE '%${q}%')`; } if (type) { filter += ` AND ComponentType = '${type}'`; } // ... rest of implementation } } ``` ### Custom Routes and Middleware Add additional routes or modify middleware: ```typescript class ExtendedRegistryServer extends ComponentRegistryAPIServer { /** * Add custom middleware */ protected setupMiddleware(): void { // Call parent middleware setup super.setupMiddleware(); // Add custom middleware this.app.use('/api/v1/admin', this.adminAuthMiddleware); // Add request logging this.app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); } /** * Add custom routes */ protected setupRoutes(): void { // Call parent route setup super.setupRoutes(); // Add admin routes this.app.post('/api/v1/admin/components', this.createComponent.bind(this)); this.app.delete('/api/v1/admin/components/:id', this.deleteComponent.bind(this)); } private async createComponent(req: Request, res: Response) { // Custom component creation logic } private async deleteComponent(req: Request, res: Response) { // Custom component deletion logic } } ``` ### Database Provider Customization Override the database setup for different providers: ```typescript class PostgresRegistryServer extends ComponentRegistryAPIServer { /** * Use PostgreSQL instead of SQL Server */ protected async setupDatabase(): Promise { // Your PostgreSQL setup logic const pgClient = new PostgreSQLClient(config); await pgClient.connect(); // Setup MemberJunction with PostgreSQL provider await setupPostgreSQLClient(pgClient); } } ``` ## Advanced Usage ### Programmatic Usage Use the server as a library in your application: ```typescript import { ComponentRegistryAPIServer } from '@memberjunction/component-registry-server'; async function startMyApp() { // Create and configure server const registryServer = new ComponentRegistryAPIServer(); // Initialize without starting await registryServer.initialize(); // Access the Express app for integration const app = registryServer.app; // Add to existing Express app existingApp.use('/registry', app); // Or start standalone await registryServer.start(); } ``` ### Multiple Registry Support Run multiple registries on different ports: ```typescript class MultiRegistryManager { private registries: Map = new Map(); async startRegistry(name: string, port: number, registryId: string) { const server = new ComponentRegistryAPIServer(); // Override configuration componentRegistrySettings.port = port; componentRegistrySettings.registryId = registryId; await server.initialize(); await server.start(); this.registries.set(name, server); } async startAll() { await this.startRegistry('public', 3200, 'public-registry-id'); await this.startRegistry('private', 3201, 'private-registry-id'); await this.startRegistry('internal', 3202, 'internal-registry-id'); } } ``` ## Configuration Reference ### componentRegistrySettings | Property | Type | Default | Description | |----------|------|---------|-------------| | `port` | number | 3200 | Port number for the server | | `enableRegistry` | boolean | false | Whether to enable the registry server | | `registryId` | string \| null | null | Optional GUID of the registry record in MJ: Component Registries | | `requireAuth` | boolean | false | Whether to require authentication for component endpoints | | `corsOrigins` | string[] | ['*'] | Allowed CORS origins | ## Database Schema The Component Registry Server uses the following MemberJunction entities: - **MJ: Component Registries**: Registry metadata and configuration - **MJ: Components**: Component definitions and specifications - **MJ: Component Libraries**: External library dependencies - **MJ: Component Dependencies**: Component-to-component dependencies ## Security Considerations ### Authentication By default, the server runs without authentication. For production use: 1. Set `requireAuth: true` in configuration 2. Extend the server class and override `checkAPIKey()` 3. Implement your authentication strategy (Bearer tokens, API keys, JWT, etc.) ### CORS Configure CORS origins appropriately for your environment: ```javascript componentRegistrySettings: { corsOrigins: [ 'https://app.example.com', 'https://admin.example.com' ] } ``` ### SQL Injection The server uses parameterized queries via MemberJunction's RunView system. User input is escaped when building filters. ## Troubleshooting ### Server Won't Start 1. Check database connection settings in `mj.config.cjs` 2. Ensure the database is accessible 3. Verify `enableRegistry` is set to `true` 4. Check port availability ### Components Not Found 1. Verify components exist in the database 2. Check component `Status` is 'Published' 3. Ensure `SourceRegistryID` is NULL for local components 4. Review the component filter if using a custom implementation ### Authentication Issues 1. Verify `requireAuth` setting matches your expectations 2. Check your `checkAPIKey` implementation 3. Ensure auth headers are being sent correctly ## Contributing Contributions are welcome! Please follow the MemberJunction contribution guidelines. ## License ISC License - See LICENSE file for details ## Support For issues and questions: - GitHub Issues: [MemberJunction/MJ](https://github.com/MemberJunction/MJ/issues) - Documentation: [docs.memberjunction.org](https://docs.memberjunction.org)