hivest-js
Version:
A simple, fast and minimalist framework for Node.js that allows you to create modular applications with dependency injection using decorators
321 lines • 13.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppModule = void 0;
const express_1 = __importDefault(require("express"));
require("reflect-metadata");
const tsyringe_1 = require("tsyringe");
const event_manager_1 = require("../event-manager");
/**
* Normalize a path by removing duplicate slashes and ensuring proper format
* @param paths - Array of path segments to join and normalize
* @returns Normalized path string
*/
function normalizePath(...paths) {
return (paths
.filter((path) => path !== undefined && path !== null)
.join('/')
.replace(/\/+/g, '/') // Replace multiple consecutive slashes with single slash
.replace(/\/$/, '') || '/'); // Remove trailing slash, but keep root as '/'
}
/**
* Main application module class that handles dependency injection,
* route registration, and module composition
*/
class AppModule {
constructor(options) {
this.options = options;
this.app = (0, express_1.default)();
this.initialized = false;
this.bootstrapped = false;
this.allProviders = [];
this.app.use(express_1.default.json());
this.eventManager = new event_manager_1.EventManager();
// Initialize providers from options
if (options.providers) {
this.allProviders = [...options.providers];
}
}
/**
* Get the Express application instance
*/
getApp() {
return this.app;
}
/**
* Get the event manager instance
*/
getEventManager() {
return this.eventManager;
}
/**
* Get all providers including parent module providers (recursive)
* Uses the Composite pattern to gather providers from the module hierarchy
*/
getAllProviders() {
const providers = [...this.allProviders];
// Add parent module providers recursively
if (this.parentModule) {
providers.push(...this.parentModule.getAllProviders());
}
return providers;
}
/**
* Share providers from parent module to this child module
* This allows child modules to access parent module providers
*/
shareParentProviders(parentProviders) {
// Add parent providers to this module's provider list
this.allProviders.push(...parentProviders);
}
/**
* Register a provider in the dependency injection container
* Supports class providers, value providers, and smart providers
*/
registerProvider(provider) {
if (provider instanceof Function) {
// Class provider - register as singleton
tsyringe_1.container.registerSingleton(provider.name, provider);
}
else if ('useValue' in provider) {
// Value provider - register as instance
tsyringe_1.container.registerInstance(provider.key, provider.useValue);
}
else {
// Smart provider - detect if provide is a class or value
const provide = provider.provide;
if (provide instanceof Function) {
// It's a class
tsyringe_1.container.registerSingleton(provider.key, provide);
}
else {
// It's a value
tsyringe_1.container.registerInstance(provider.key, provide);
}
}
}
/**
* Register all providers in the dependency injection container
* Uses the Strategy pattern to handle different provider types
*/
registerAllProviders() {
const allProviders = this.getAllProviders();
for (const provider of allProviders) {
this.registerProvider(provider);
}
}
/**
* Check if a controller is marked as middleware
*/
isMiddlewareController(controller) {
return Reflect.getMetadata('controller:isMiddleware', controller) === true;
}
/**
* Check if a controller is marked as error handler middleware
*/
isErrorHandlerController(controller) {
return Reflect.getMetadata('controller:isErrorHandler', controller) === true;
}
/**
* Process and register a single controller item (route, middleware, or error handler)
* Uses the Strategy pattern to handle different item types
*/
processControllerItem(item, context) {
if (item.type === 'route') {
this.registerRoute(item, context);
}
else if (item.type === 'middleware') {
this.registerMiddleware(item, context);
}
else if (item.type === 'errorHandler') {
this.registerErrorHandler(item, context);
}
}
/**
* Register a route in the Express application
*/
registerRoute(item, context) {
const { modulePath, controllerPath, controllerInstance, app } = context;
const routePath = normalizePath(modulePath, controllerPath, item.path || '');
console.log(`Registering route: ${item.method?.toUpperCase()} ${routePath}`);
app[item.method](routePath, async (req, res, next) => {
try {
await controllerInstance[item.propertyKey]({ req, res });
}
catch (error) {
next(error);
}
});
}
/**
* Register a middleware in the Express application
*/
registerMiddleware(item, context) {
const { controllerInstance, app } = context;
console.log(`Registering middleware: ${item.propertyKey}`);
app.use(async (req, res, next) => {
try {
await controllerInstance[item.propertyKey]({ req, res, next });
}
catch (error) {
next(error);
}
});
}
/**
* Register an error handler in the Express application
*/
registerErrorHandler(item, context) {
const { controllerInstance, app } = context;
console.log(`Registering error handler: ${item.propertyKey}`);
app.use(async (err, req, res, next) => {
try {
await controllerInstance[item.propertyKey]({ req, res, next, err });
}
catch (error) {
next(error);
}
});
}
/**
* Process and register all controllers from a module
* Uses the Template Method pattern to process controllers consistently
* Now automatically detects and processes middleware classes and error handlers
*/
processControllers(controllers, modulePath) {
for (const controller of controllers) {
const controllerInstance = tsyringe_1.container.resolve(controller);
const controllerPath = Reflect.getMetadata('controller:path', controller) || '';
const items = Reflect.getMetadata('controller:items', controller) || [];
// Register event listeners and emitters
this.eventManager.registerListeners(controllerInstance);
this.eventManager.registerEmitters(controllerInstance);
// If this is a middleware controller, ensure all methods without decorators are treated as middleware
if (this.isMiddlewareController(controller)) {
this.ensureMiddlewareMethods(controller, items);
}
// If this is an error handler controller, get error handler items
if (this.isErrorHandlerController(controller)) {
const errorHandlerItems = Reflect.getMetadata('controller:errorHandlers', controller) || [];
items.push(...errorHandlerItems);
}
const context = {
modulePath,
controllerPath,
controllerInstance,
app: this.app,
};
for (const item of items) {
this.processControllerItem(item, context);
}
}
}
/**
* Ensure all methods without decorators in a middleware controller are treated as middleware
*/
ensureMiddlewareMethods(controller, existingItems) {
const existingMethodNames = existingItems.map((item) => item.propertyKey);
// Get all method names from the prototype
const methodNames = Object.getOwnPropertyNames(controller.prototype).filter((name) => name !== 'constructor' && typeof controller.prototype[name] === 'function');
// Find methods that don't have any decorators (not in existingItems)
const methodsWithoutDecorators = methodNames.filter((methodName) => !existingMethodNames.includes(methodName));
// Add middleware items for methods without decorators
const newMiddlewareItems = methodsWithoutDecorators.map((methodName) => ({
type: 'middleware',
handler: controller.prototype[methodName],
propertyKey: methodName,
}));
// Combine existing items (routes) with new middleware items
const allItems = [...existingItems, ...newMiddlewareItems];
// Update the metadata with combined items
Reflect.defineMetadata('controller:items', allItems, controller);
}
/**
* Bootstrap imported modules (parent modules)
* Uses the Composite pattern to handle module hierarchy
*/
async bootstrapImportedModules(imports) {
const importedModuleInstances = [];
for (const importedModule of imports) {
const moduleInstance = importedModule instanceof AppModule ? importedModule : new importedModule();
// Set this as parent for the imported module (Composite pattern)
moduleInstance.parentModule = this;
// Share providers from parent to child module
moduleInstance.shareParentProviders(this.allProviders);
importedModuleInstances.push(moduleInstance);
// Register the imported module's own providers in the DI container
moduleInstance.registerAllProviders();
// Bootstrap the imported module as a child (without re-registering providers)
await moduleInstance.bootstrapChild();
}
return importedModuleInstances;
}
/**
* Process controllers from imported modules
* Uses the Visitor pattern to process external module controllers
*/
processImportedModuleControllers(importedModuleInstances, modulePath) {
for (const moduleInstance of importedModuleInstances) {
const importedControllers = moduleInstance.options.controllers || [];
const importedPath = moduleInstance.options.path || '/';
const fullModulePath = normalizePath(modulePath, importedPath);
this.processControllers(importedControllers, fullModulePath);
}
}
/**
* Bootstrap the application module
* Orchestrates the entire initialization process using multiple patterns
*/
async bootstrap() {
if (this.bootstrapped)
return this;
const { path: modulePath = '/', controllers = [], imports = [] } = this.options;
// Step 1: Register all providers FIRST (including inherited ones) BEFORE any controllers are resolved
this.registerAllProviders();
// Step 2: Bootstrap imported modules (Composite pattern)
const importedModuleInstances = await this.bootstrapImportedModules(imports);
// Step 3: Process local controllers FIRST (middleware should be registered before routes)
this.processControllers(controllers, modulePath);
// Step 4: Process controllers from imported modules (routes)
this.processImportedModuleControllers(importedModuleInstances, modulePath);
this.bootstrapped = true;
return this;
}
/**
* Bootstrap child modules without re-registering providers
* This is used by child modules to avoid duplicate provider registration
*/
async bootstrapChild() {
if (this.bootstrapped)
return this;
const { path: modulePath = '/', controllers = [], imports = [] } = this.options;
// Skip provider registration for child modules since they're already registered by parent
// Step 1: Bootstrap imported modules (Composite pattern)
const importedModuleInstances = await this.bootstrapImportedModules(imports);
// Step 2: Process local controllers FIRST (middleware should be registered before routes)
this.processControllers(controllers, modulePath);
// Step 3: Process controllers from imported modules (routes)
this.processImportedModuleControllers(importedModuleInstances, modulePath);
this.bootstrapped = true;
return this;
}
/**
* Start the HTTP server
* Ensures proper initialization order
*/
async listen(port) {
if (this.initialized)
return this;
if (!this.bootstrapped)
await this.bootstrap();
this.app.listen(port, () => {
console.info(`Server is running on port ${port}`);
});
this.initialized = true;
return this;
}
}
exports.AppModule = AppModule;
//# sourceMappingURL=module.js.map