REST API server for serving MemberJunction interactive components. Provides a standardized API for discovering, searching, and retrieving components and their specifications.
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.
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
npm install @memberjunction/component-registry-server
Add configuration to your mj.config.cjs file in the project root:
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
}
};
# 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
GET /api/v1/registry
{
"name": "Local Component Registry",
"description": "MemberJunction Component Registry",
"version": "v1",
"requiresAuth": false
}
GET /api/v1/health
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00.000Z",
"version": "v1",
"componentCount": 42
}
GET /api/v1/components
{
"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
}
GET /api/v1/components/search?q=revenue&type=Dashboard
{
"results": [...],
"total": 5,
"query": "revenue"
}
GET /api/v1/components/:namespace/:name
{
"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
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.
Create a subclass and override the checkAPIKey method:
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<boolean> {
// 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<boolean> {
// 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();
Override the getComponentFilter method to control which components are served:
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')";
}
}
Override the route handlers to pull components from different tables or entities:
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<void> {
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<void> {
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<void> {
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
}
}
Add additional routes or modify middleware:
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
}
}
Override the database setup for different providers:
class PostgresRegistryServer extends ComponentRegistryAPIServer {
/**
* Use PostgreSQL instead of SQL Server
*/
protected async setupDatabase(): Promise<void> {
// Your PostgreSQL setup logic
const pgClient = new PostgreSQLClient(config);
await pgClient.connect();
// Setup MemberJunction with PostgreSQL provider
await setupPostgreSQLClient(pgClient);
}
}
Use the server as a library in your application:
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();
}
Run multiple registries on different ports:
class MultiRegistryManager {
private registries: Map<string, ComponentRegistryAPIServer> = 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');
}
}
| 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 |
The Component Registry Server uses the following MemberJunction entities:
By default, the server runs without authentication. For production use:
requireAuth: true in configurationcheckAPIKey()Configure CORS origins appropriately for your environment:
componentRegistrySettings: {
corsOrigins: [
'https://app.example.com',
'https://admin.example.com'
]
}
The server uses parameterized queries via MemberJunction's RunView system. User input is escaped when building filters.
mj.config.cjsenableRegistry is set to trueStatus is 'Published'SourceRegistryID is NULL for local componentsrequireAuth setting matches your expectationscheckAPIKey implementationContributions are welcome! Please follow the MemberJunction contribution guidelines.
ISC License - See LICENSE file for details
For issues and questions: