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
JavaScript
;
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