nooblyjs-core
Version:
A powerful set of modular Node.js backend services with singleton pattern - caching, logging, data serving, filing, measuring, notifying, queueing, scheduling, searching, workflow, and working services.
492 lines (427 loc) • 17.2 kB
JavaScript
/**
* @fileoverview NooblyJS Core - Service Registry
* A powerful set of modular Node.js backend services with singleton pattern.
*/
const EventEmitter = require('events');
const express = require('express');
const path = require('path');
const {
createApiKeyAuthMiddleware,
generateApiKey,
} = require('./src/middleware/apiKeyAuth');
const { createServicesAuthMiddleware } = require('./src/middleware/servicesAuth');
class ServiceRegistry {
constructor() {
this.services = new Map();
this.serviceDependencies = new Map();
this.initialized = false;
this.eventEmitter = new EventEmitter();
this.dependenciesInitialized = false;
}
/**
* Initializes the service registry with an Express app
* @param {Object} expressApp - Express application instance
* @param {Object} eventEmitter - EventEmitter instance
* @param {Object} globalOptions - Global configuration options
*/
initialize(expressApp, eventEmitter, globalOptions = {}) {
if (this.initialized) {
return this;
}
this.expressApp = expressApp;
this.eventEmitter = eventEmitter;
this.globalOptions = {
'express-app': expressApp,
...globalOptions,
};
// Initialize service dependencies
this.initializeServiceDependencies();
// Setup API key authentication if configured
if (globalOptions.apiKeys && globalOptions.apiKeys.length > 0) {
this.authMiddleware = createApiKeyAuthMiddleware(
{
apiKeys: globalOptions.apiKeys,
requireApiKey: globalOptions.requireApiKey !== false,
excludePaths: globalOptions.excludePaths || [
'/services/*/status',
'/services/',
'/services/*/views/*',
],
},
this.eventEmitter,
);
// Store auth config for services to use
this.globalOptions.authMiddleware = this.authMiddleware;
// Log API key authentication setup
this.eventEmitter.emit('api-auth-setup', {
message: 'API key authentication enabled',
keyCount: globalOptions.apiKeys.length,
requireApiKey: globalOptions.requireApiKey !== false,
});
}
// Initialize services auth middleware
this.servicesAuthMiddleware = createServicesAuthMiddleware(this);
// Serve the service registry landing page (protected) - MUST come before static middleware
this.expressApp.get('/services/', this.servicesAuthMiddleware, (req, res) => {
res.sendFile(path.join(__dirname, 'src/views', 'index.html'));
});
// Serve static files from the views directory for caching service (excluding index.html)
expressApp.use(
'/services/',
(req, res, next) => {
// Exclude index.html from static serving since it's handled by the protected route above
if (req.path === '/' || req.path === '/index.html') {
return next();
}
next();
},
express.static(path.join(__dirname, 'src/views')),
);
// Serve the service registry landing page
this.expressApp.get('/services/documentation', (req, res) => {
res.sendFile(path.join(__dirname, './jsdoc', 'index.html'));
});
this.initialized = true;
return this;
}
/**
* Initialize service dependency definitions according to the architecture hierarchy
* Level 0: Foundation Services (No Dependencies)
* Level 1: Infrastructure Services (Use Foundation)
* Level 2: Business Logic Services (Use Infrastructure)
* Level 3: Application Services (Use Business Logic)
* Level 4: Integration Services (Use Application)
*/
initializeServiceDependencies() {
if (this.dependenciesInitialized) {
return;
}
// Level 0 services (Foundation - No dependencies)
this.serviceDependencies.set('logging', []);
this.serviceDependencies.set('filing', []);
this.serviceDependencies.set('measuring', []);
// Level 1 services (Infrastructure - Use foundation services)
this.serviceDependencies.set('caching', ['logging']);
this.serviceDependencies.set('dataserve', ['logging', 'filing']);
this.serviceDependencies.set('working', ['logging']);
// Level 2 services (Business Logic - Use infrastructure services)
this.serviceDependencies.set('queueing', ['logging', 'caching', 'dataserve']);
this.serviceDependencies.set('scheduling', ['logging', 'measuring', 'queueing']);
this.serviceDependencies.set('searching', ['logging', 'caching', 'dataserve']);
// Level 3 services (Application - Use business logic services)
this.serviceDependencies.set('workflow', ['logging', 'queueing', 'scheduling', 'measuring']);
this.serviceDependencies.set('notifying', ['logging', 'queueing', 'scheduling']);
this.serviceDependencies.set('authservice', ['logging', 'caching', 'dataserve']);
// Level 4 services (Integration - Use application services)
this.serviceDependencies.set('aiservice', ['logging', 'caching', 'workflow', 'queueing']);
this.dependenciesInitialized = true;
// Emit event for dependency system initialization
this.eventEmitter.emit('dependencies:initialized', {
message: 'Service dependency hierarchy initialized',
dependencies: Object.fromEntries(this.serviceDependencies)
});
}
/**
* Gets or creates a service instance (singleton pattern with dependency injection)
* @param {string} serviceName - Name of the service
* @param {string} providerType - Type of provider to use
* @param {Object} options - Service-specific options
* @returns {Object} Service instance
*/
getService(serviceName, providerType = 'memory', options = {}) {
if (!this.initialized) {
throw new Error(
'ServiceRegistry must be initialized before getting services',
);
}
const serviceKey = `${serviceName}:${providerType}`;
if (this.services.has(serviceKey)) {
return this.services.get(serviceKey);
}
// Get dependencies for this service
const dependencies = this.resolveDependencies(serviceName, providerType);
const mergedOptions = {
...this.globalOptions,
...options,
dependencies
};
let service;
try {
const serviceFactory = require(`${__dirname}/src/${serviceName}`);
service = serviceFactory(providerType, mergedOptions, this.eventEmitter);
} catch (error) {
throw new Error(
`Failed to create service '${serviceName}' with provider '${providerType}': ${error.message}`,
);
}
this.services.set(serviceKey, service);
// Emit event for service creation with dependencies
this.eventEmitter.emit('service:created', {
serviceName,
providerType,
dependenciesCount: Object.keys(dependencies).length,
dependencyNames: Object.keys(dependencies)
});
return service;
}
/**
* Get the default provider type for a specific service
* @param {string} serviceName - Name of the service
* @returns {string} Default provider type for the service
*/
getDefaultProviderType(serviceName) {
const defaultProviders = {
'logging': 'console',
'filing': 'local',
'measuring': 'memory',
'caching': 'memory',
'dataserve': 'memory',
'working': 'memory',
'queueing': 'memory',
'scheduling': 'memory',
'searching': 'memory',
'workflow': 'memory',
'notifying': 'memory',
'authservice': 'file',
'aiservice': 'claude'
};
return defaultProviders[serviceName] || 'memory';
}
/**
* Resolve dependencies for a service by creating dependent services first
* @param {string} serviceName - Name of the service
* @param {string} requestedProviderType - Provider type requested for the main service
* @returns {Object} Object containing dependency service instances
*/
resolveDependencies(serviceName, requestedProviderType = 'memory') {
const dependencies = {};
const requiredDependencies = this.serviceDependencies.get(serviceName) || [];
for (const depServiceName of requiredDependencies) {
const depProviderType = this.getDefaultProviderType(depServiceName);
const depServiceKey = `${depServiceName}:${depProviderType}`;
// Check if dependency is already created
if (this.services.has(depServiceKey)) {
dependencies[depServiceName] = this.services.get(depServiceKey);
} else {
// Recursively create dependency with appropriate provider type
dependencies[depServiceName] = this.getService(depServiceName, depProviderType);
}
}
return dependencies;
}
/**
* Get service initialization order using topological sort
* This ensures dependencies are initialized before services that depend on them
* @returns {Array<string>} Array of service names in initialization order
*/
getServiceInitializationOrder() {
const visited = new Set();
const visiting = new Set();
const order = [];
const visit = (serviceName) => {
if (visiting.has(serviceName)) {
throw new Error(`Circular dependency detected involving service: ${serviceName}`);
}
if (!visited.has(serviceName)) {
visiting.add(serviceName);
const dependencies = this.serviceDependencies.get(serviceName) || [];
for (const dependency of dependencies) {
visit(dependency);
}
visiting.delete(serviceName);
visited.add(serviceName);
order.push(serviceName);
}
};
// Visit all services
for (const serviceName of this.serviceDependencies.keys()) {
visit(serviceName);
}
return order;
}
/**
* Validates that the dependency graph has no circular dependencies
* @returns {boolean} True if dependency graph is valid
* @throws {Error} If circular dependencies are detected
*/
validateDependencies() {
try {
this.getServiceInitializationOrder();
return true;
} catch (error) {
throw new Error(`Dependency validation failed: ${error.message}`);
}
}
/**
* Get the caching service
* @param {string} providerType - 'memory', 'redis', or 'memcached'
* @param {Object} options - Provider-specific options
* @returns {Object} Cache service instance
*/
cache(providerType = 'memory', options = {}) {
return this.getService('caching', providerType, options);
}
/**
* Get the logging service
* @param {string} providerType - 'console' or 'file'
* @param {Object} options - Provider-specific options
* @returns {Object} Logger service instance
*/
logger(providerType = 'console', options = {}) {
return this.getService('logging', providerType, options);
}
/**
* Get the data serving service
* @param {string} providerType - 'memory', 'simpledb', 'file', 'mongodb', or 'documentdb'
* @param {Object} options - Provider-specific options
* @param {string} options.connectionString - MongoDB/DocumentDB connection string (for mongodb/documentdb provider)
* @param {string} options.database - Database name (for mongodb/documentdb provider)
* @param {string} options.host - DocumentDB host (for documentdb provider, defaults to '127.0.0.1')
* @param {number} options.port - DocumentDB port (for documentdb provider, defaults to 10260)
* @param {string} options.username - Username for authentication (for documentdb provider)
* @param {string} options.password - Password for authentication (for documentdb provider)
* @param {boolean} options.ssl - Enable SSL connection (for documentdb provider)
* @returns {Object} DataServe service instance
*/
dataServe(providerType = 'memory', options = {}) {
return this.getService('dataserve', providerType, options);
}
/**
* Get the filing service
* @param {string} providerType - 'local', 'ftp', 's3', 'git', or 'sync'
* @param {Object} options - Provider-specific options
* @returns {Object} Filing service instance
*/
filing(providerType = 'local', options = {}) {
return this.getService('filing', providerType, options);
}
/**
* Get the filer service (alias for filing service)
* @param {string} providerType - 'local', 'ftp', 's3', 'git', or 'sync'
* @param {Object} options - Provider-specific options
* @returns {Object} Filer service instance
*/
filer(providerType = 'local', options = {}) {
return this.getService('filing', providerType, options);
}
/**
* Get the measuring service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Measuring service instance
*/
measuring(providerType = 'memory', options = {}) {
return this.getService('measuring', providerType, options);
}
/**
* Get the notifying service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Notifying service instance
*/
notifying(providerType = 'memory', options = {}) {
return this.getService('notifying', providerType, options);
}
/**
* Get the queueing service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Queue service instance
*/
queue(providerType = 'memory', options = {}) {
return this.getService('queueing', providerType, options);
}
/**
* Get the scheduling service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Scheduling service instance
*/
scheduling(providerType = 'memory', options = {}) {
return this.getService('scheduling', providerType, options);
}
/**
* Get the searching service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Searching service instance
*/
searching(providerType = 'memory', options = {}) {
return this.getService('searching', providerType, options);
}
/**
* Get the workflow service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Workflow service instance
*/
workflow(providerType = 'memory', options = {}) {
return this.getService('workflow', providerType, options);
}
/**
* Get the working service
* @param {string} providerType - 'memory'
* @param {Object} options - Provider-specific options
* @returns {Object} Working service instance
*/
working(providerType = 'memory', options = {}) {
return this.getService('working', providerType, options);
}
/**
* Get the AI service
* @param {string} providerType - 'claude', 'chatgpt', or 'ollama'
* @param {Object} options - Provider-specific options
* @param {string} options.apiKey - API key for the provider (required for claude/chatgpt)
* @param {string} options.model - Model to use (optional)
* @returns {Object} AI service instance
*/
aiservice(providerType = 'claude', options = {}) {
return this.getService('aiservice', providerType, options);
}
/**
* Get the authentication service
* @param {string} providerType - 'file', 'memory', 'passport', or 'google'
* @param {Object} options - Provider-specific options
* @param {string} options.dataDir - Directory for user data files (for file provider)
* @param {string} options.clientID - Google OAuth client ID (for google provider)
* @param {string} options.clientSecret - Google OAuth client secret (for google provider)
* @param {string} options.callbackURL - OAuth callback URL (for google provider)
* @param {boolean} options.createDefaultAdmin - Create default admin user (for memory provider)
* @returns {Object} Auth service instance
*/
authservice(providerType = 'file', options = {}) {
return this.getService('authservice', providerType, options);
}
/**
* Get the event emitter for inter-service communication
* @returns {EventEmitter} The global event emitter
*/
getEventEmitter() {
return this.eventEmitter;
}
/**
* Lists all initialized services
* @returns {Array} Array of service keys
*/
listServices() {
return Array.from(this.services.keys());
}
/**
* Generate a new API key
* @param {number} length - Length of the API key (default: 32)
* @returns {string} Generated API key
*/
generateApiKey(length = 32) {
return generateApiKey(length);
}
/**
* Clears all service instances (useful for testing)
*/
reset() {
this.services.clear();
this.initialized = false;
}
}
// Export singleton instance
const serviceRegistry = new ServiceRegistry();
module.exports = serviceRegistry;