UNPKG

artmapper

Version:

Spring Boot clone for Node.js with TypeScript/JavaScript - JPA-like ORM, Lombok decorators, dependency injection, and MySQL support

342 lines 15.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpringApplication = void 0; exports.run = run; require("reflect-metadata"); const express_1 = __importDefault(require("express")); const Container_1 = require("../di/Container"); const DatabaseConfig_1 = require("../database/DatabaseConfig"); const SchemaGenerator_1 = require("../database/SchemaGenerator"); const web_1 = require("../decorators/web"); const component_1 = require("../decorators/component"); class SpringApplication { constructor(config = {}) { this.app = (0, express_1.default)(); this.container = Container_1.Container.getInstance(); this.databaseManager = DatabaseConfig_1.DatabaseManager.getInstance(); this.config = { port: 3000, basePath: '/', ...config, }; this.setupMiddleware(); } /** * Setup Express middleware */ setupMiddleware() { // Body parser with increased limit for large payloads this.app.use(express_1.default.json({ limit: '10mb' })); this.app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' })); // CORS middleware this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); } /** * Initialize database connection */ async initializeDatabase() { if (this.config.database) { await this.databaseManager.initialize(this.config.database); this.container.setPool(this.databaseManager.getPool()); // Auto-generate schema if enabled and entities are provided if (this.config.autoGenerateSchema && this.config.entities && this.config.entities.length > 0) { const schemaGenerator = new SchemaGenerator_1.SchemaGenerator(this.databaseManager.getPool()); await schemaGenerator.generateSchema(this.config.entities); } } } /** * Register all components */ registerComponents() { // Register repositories first (they might be dependencies) if (this.config.repositories) { this.config.repositories.forEach(RepoClass => { if (this.config.database) { const pool = this.databaseManager.getPool(); const instance = new RepoClass(pool); this.container.registerBean(RepoClass, instance); } else { this.container.registerBean(RepoClass); } }); } // Register services if (this.config.services) { this.config.services.forEach(ServiceClass => { this.container.registerBean(ServiceClass); }); } // Register components if (this.config.components) { this.config.components.forEach(ComponentClass => { this.container.registerBean(ComponentClass); }); } // Register controllers last (they depend on services) if (this.config.controllers) { this.config.controllers.forEach(ControllerClass => { this.container.registerBean(ControllerClass); }); } } /** * Register all routes from controllers */ registerRoutes() { if (!this.config.controllers) return; this.config.controllers.forEach(ControllerClass => { const controllerMetadata = Reflect.getMetadata(component_1.CONTROLLER_METADATA_KEY, ControllerClass); const controllerPath = controllerMetadata?.path || ''; const controller = this.container.getBean(ControllerClass); // Register GET routes const getRoutes = Reflect.getMetadata(web_1.GET_METADATA_KEY, ControllerClass) || []; getRoutes.forEach((route) => { // Handle empty path - ensure no trailing slash issues let routePath = route.path || ''; if (routePath === '' && controllerPath.endsWith('/')) { routePath = '/'; } let fullPath = `${this.config.basePath}${controllerPath}${routePath}`.replace(/\/+/g, '/'); // Remove trailing slash except for root if (fullPath !== '/' && fullPath.endsWith('/')) { fullPath = fullPath.slice(0, -1); } console.log(` Registering GET ${fullPath} -> ${ControllerClass.name}.${route.handler}`); this.app.get(fullPath, this.createRouteHandler(controller, ControllerClass, route.handler, route.path)); }); // Register POST routes const postRoutes = Reflect.getMetadata(web_1.POST_METADATA_KEY, ControllerClass) || []; postRoutes.forEach((route) => { // Handle empty path - ensure no trailing slash issues let routePath = route.path || ''; if (routePath === '' && controllerPath.endsWith('/')) { routePath = '/'; } let fullPath = `${this.config.basePath}${controllerPath}${routePath}`.replace(/\/+/g, '/'); // Remove trailing slash except for root if (fullPath !== '/' && fullPath.endsWith('/')) { fullPath = fullPath.slice(0, -1); } console.log(` Registering POST ${fullPath} -> ${ControllerClass.name}.${route.handler}`); this.app.post(fullPath, this.createRouteHandler(controller, ControllerClass, route.handler, route.path)); }); // Register PUT routes const putRoutes = Reflect.getMetadata(web_1.PUT_METADATA_KEY, ControllerClass) || []; putRoutes.forEach((route) => { let routePath = route.path || ''; let fullPath = `${this.config.basePath}${controllerPath}${routePath}`.replace(/\/+/g, '/'); if (fullPath !== '/' && fullPath.endsWith('/')) { fullPath = fullPath.slice(0, -1); } console.log(` Registering PUT ${fullPath} -> ${ControllerClass.name}.${route.handler}`); this.app.put(fullPath, this.createRouteHandler(controller, ControllerClass, route.handler, route.path)); }); // Register DELETE routes const deleteRoutes = Reflect.getMetadata(web_1.DELETE_METADATA_KEY, ControllerClass) || []; deleteRoutes.forEach((route) => { let routePath = route.path || ''; let fullPath = `${this.config.basePath}${controllerPath}${routePath}`.replace(/\/+/g, '/'); if (fullPath !== '/' && fullPath.endsWith('/')) { fullPath = fullPath.slice(0, -1); } console.log(` Registering DELETE ${fullPath} -> ${ControllerClass.name}.${route.handler}`); this.app.delete(fullPath, this.createRouteHandler(controller, ControllerClass, route.handler, route.path)); }); // Register PATCH routes const patchRoutes = Reflect.getMetadata(web_1.PATCH_METADATA_KEY, ControllerClass) || []; patchRoutes.forEach((route) => { let routePath = route.path || ''; let fullPath = `${this.config.basePath}${controllerPath}${routePath}`.replace(/\/+/g, '/'); if (fullPath !== '/' && fullPath.endsWith('/')) { fullPath = fullPath.slice(0, -1); } console.log(` Registering PATCH ${fullPath} -> ${ControllerClass.name}.${route.handler}`); this.app.patch(fullPath, this.createRouteHandler(controller, ControllerClass, route.handler, route.path)); }); }); } /** * Create a route handler with parameter injection */ createRouteHandler(controller, ControllerClass, handlerName, routePath) { return async (req, res, next) => { try { const handler = controller[handlerName]; if (!handler) { return res.status(500).json({ error: `Handler ${handlerName} not found` }); } // Extract path variables from route path const pathVarNames = []; const pathVarRegex = /:(\w+)/g; let match; while ((match = pathVarRegex.exec(routePath)) !== null) { pathVarNames.push(match[1]); } // Get parameter metadata from the class prototype (where decorators store it) const prototype = ControllerClass.prototype; const requestBodyParams = Reflect.getMetadata(web_1.REQUEST_BODY_METADATA_KEY, prototype, handlerName) || {}; const requestParamParams = Reflect.getMetadata(web_1.REQUEST_PARAM_METADATA_KEY, prototype, handlerName) || {}; const pathVariableParams = Reflect.getMetadata(web_1.PATH_VARIABLE_METADATA_KEY, prototype, handlerName) || {}; const requestHeaderParams = Reflect.getMetadata(web_1.REQUEST_HEADER_METADATA_KEY, prototype, handlerName) || {}; // Build arguments array const paramTypes = Reflect.getMetadata('design:paramtypes', prototype, handlerName) || []; const args = []; // Check if handler accepts req/res directly (JavaScript style without parameter decorators) const hasDirectReqRes = paramTypes.length === 2 && (paramTypes[0] === Request || paramTypes[0] === Object) && (paramTypes[1] === Response || paramTypes[1] === Object); if (hasDirectReqRes && Object.keys(requestBodyParams).length === 0 && Object.keys(requestParamParams).length === 0 && Object.keys(pathVariableParams).length === 0) { // JavaScript style - pass req/res directly const result = await handler.call(controller, req, res); // If handler didn't send response, check return value if (result !== undefined && !res.headersSent) { if (typeof result === 'object') { res.json(result); } else { res.send(result); } } return; } // TypeScript style - use parameter decorators for (let i = 0; i < paramTypes.length; i++) { if (requestBodyParams[i]) { // Ensure body is parsed (should be done by express.json() middleware) if ((!req.body || Object.keys(req.body).length === 0) && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) { console.warn(`Warning: req.body is empty for ${req.method} ${req.path}`); console.warn(` Content-Type: ${req.headers['content-type']}`); console.warn(` Body length: ${req.headers['content-length'] || 0}`); } args[i] = req.body || {}; } else if (requestParamParams[i]) { const paramInfo = requestParamParams[i]; args[i] = req.query[paramInfo.name]; } else if (pathVariableParams[i]) { const paramInfo = pathVariableParams[i]; // Try to get from params by name, or by index if name matches path variable args[i] = req.params[paramInfo.name] || (pathVarNames.length > 0 && req.params[pathVarNames[0]]) || Object.values(req.params)[0]; } else if (requestHeaderParams[i]) { const paramInfo = requestHeaderParams[i]; args[i] = req.headers[paramInfo.name.toLowerCase()]; } else if (paramTypes[i] === Request) { args[i] = req; } else if (paramTypes[i] === Response) { args[i] = res; } else { args[i] = undefined; } } // Call handler let result; try { result = await handler.apply(controller, args); } catch (handlerError) { console.error(`Handler error in ${ControllerClass.name}.${handlerName}:`, handlerError); throw handlerError; } // Send response if (!res.headersSent) { // Handle null responses - return 404 for GET requests if (result === null && req.method === 'GET') { return res.status(404).json({ error: 'Resource not found' }); } if (result !== undefined && result !== null) { if (typeof result === 'object') { res.json(result); } else { res.send(result); } } else if (result === null) { // For non-GET requests, null is acceptable res.status(200).json(null); } } } catch (error) { next(error); } }; } /** * Add custom middleware */ use(middleware) { this.app.use(middleware); } /** * Get the Express app instance */ getApp() { return this.app; } /** * Start the application */ async run() { // Initialize database await this.initializeDatabase(); // Register components this.registerComponents(); // Register routes this.registerRoutes(); // Error handling middleware this.app.use((err, req, res, next) => { console.error('Error:', err); res.status(err.status || 500).json({ error: err.message || 'Internal server error', ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }); }); // Start server const port = this.config.port || 3000; this.app.listen(port, () => { console.log(`✓ Spring Boot TS application running on port ${port}`); console.log(`✓ Base path: ${this.config.basePath}`); }); } /** * Stop the application */ async stop() { await this.databaseManager.close(); } } exports.SpringApplication = SpringApplication; /** * Static method to run the application */ async function run(applicationClass, config) { const app = new applicationClass(); await app.run(); } //# sourceMappingURL=SpringApplication.js.map