├── .env.example ├── .npmignore ├── .vscode └── settings.json ├── README.MD ├── cli └── index.js ├── example ├── example.domain │ ├── commands │ │ └── Another.command.js │ └── environments │ │ ├── _sample.js │ │ ├── default.js │ │ ├── development.js │ │ ├── production.js │ │ └── test.js ├── helper.js ├── multi-micro.js ├── test.micro.js.js ├── user.domain │ ├── .env.exmaple │ ├── .vscode │ │ └── settings.json │ ├── commands │ │ ├── Login.command.js │ │ └── User.command.js │ ├── environments │ │ ├── configs │ │ │ └── sqliteIntitalQueryConfig.js │ │ ├── default.js │ │ ├── development.js │ │ ├── production.js │ │ └── test.js │ ├── events │ │ └── UserCreated.event.js │ ├── middlewares │ │ ├── AdminRole.middleware.js │ │ ├── ModeratorRole.middleware.js │ │ ├── UserAccess.middleware.js │ │ └── UserRole.middleware.js │ ├── models │ │ ├── aggregates │ │ │ └── UserAggregate.js │ │ ├── entities │ │ │ ├── Auth.js │ │ │ └── Profile.js │ │ └── valueObjects │ │ │ ├── Birthday.js │ │ │ ├── Email.js │ │ │ └── PhoneNumber.js │ ├── package.json │ ├── packages │ │ └── hash.js │ ├── repositories │ │ └── UserRepository.js │ ├── services │ │ ├── LoginService.js │ │ └── UserService.js │ └── test │ │ ├── crud.js │ │ ├── index.html │ │ └── login.js └── user.micro.js ├── index.js ├── package.json ├── shared └── utils │ └── generateCertificates.js └── src ├── adapters ├── BaseLauncher.js ├── BaseServer.js ├── MainLauncher.js ├── http │ ├── Http2Server.js │ ├── Http3Server.js │ ├── HttpLauncher.js │ └── HttpServer.js └── rpc │ ├── RpcLauncher.js │ └── RpcServer.js ├── application ├── Application.js ├── command │ ├── Command.js │ ├── CommandDispatcher.js │ ├── CommandParser.js │ └── CommandRouter.js ├── database │ ├── Database.js │ ├── DatabaseAdapter.js │ ├── DatabaseInterface.js │ └── adapters │ │ ├── MongoInterface.js │ │ ├── MySqlInterface.js │ │ ├── RedisInterface.js │ │ └── SqlLiteInterface.js ├── events │ ├── EventManager.js │ ├── Events.js │ └── interfaces │ │ ├── EventEmitter2.js │ │ ├── Kafka.js │ │ └── RabbitMQ.js ├── extends │ └── BaseEntity.js ├── loader │ ├── Loader.js │ ├── LoaderResolver.js │ └── Packages.js └── middleware │ └── MiddlewareManager.js ├── config ├── ConfigCenter.js └── environments │ ├── default.js │ ├── development.js │ ├── production.js │ └── test.js ├── main.js ├── utils ├── Hash.js ├── Logger.js ├── SessionManager.js ├── SessionStorage.js └── ToolManager.js └── v └── command ├── command.v ├── command_dispatcher.v ├── command_router.v ├── commandparser.v ├── main.v └── toolmanager.v /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules/ 3 | credentials/ 4 | dist/ 5 | package-lock.json 6 | .env 7 | storage/ 8 | db.** 9 | db.* 10 | .env** -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#2C3007", 4 | "titleBar.activeBackground": "#3E4309", 5 | "titleBar.activeForeground": "#F9FBE6" 6 | } 7 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## Hex ⬡ 2 | ### A DDD Hexagonal Architecture for Flexible Web Applications 3 | 4 | Welcome to Hex, a pure JavaScript implementation of a backend microservice built on Domain-Driven Design (DDD) principles. It simplifies building scalable, modular, and environment-aware backend applications. 5 | 6 | `and also we’d love for you to be part of the Hex journey!` 7 | 8 | **Hex-Micro** 9 | 10 | ## Installation 11 | 12 | Install the package via npm: 13 | 14 | ```bash 15 | npm install hex-micro 16 | ``` 17 | Install the package via bun: 18 | 19 | ```bash 20 | bun install hex-micro 21 | ``` 22 | 23 | ## Project Setup CLI 24 | ### A command-line interface (CLI) for managing Hex Micro projects and services. 25 | 26 | 27 | Ensure that your environment directory contains at least one configuration file `(default.js, development.js, production.js, or test.js)`. 28 | Modify the templateDir path in the script if the template is stored in a different location. 29 | 30 | #### Installation 31 | install package and link the CLI tool if needed: 32 | ```bash 33 | npm install hex-micro (-g optinal) 34 | npm link (if needed) 35 | ``` 36 | #### Commands: 37 | ```bash 38 | hex start 39 | ``` 40 | Starts the Hex Micro service using the specified environment configuration. 41 | or you can first use `hex create ` then start following directory 42 | #### Example: 43 | ```bash 44 | hex start 45 | ``` 46 | Checks the specified environment path for required configuration files. 47 | Launches the Hex Micro service with the provided settings. 48 | Logs: 49 | Success: Service started successfully. 50 | Error: Detailed error messages when the service cannot start due to missing files or invalid paths. 51 | ```bash 52 | hex create 53 | ``` 54 | Creates a new Hex Micro project at the specified location. 55 | Example: 56 | ```bash 57 | hex create my-hex-project 58 | ``` 59 | 60 | This command: 61 | 62 | Copies the template directory to the specified path. 63 | Initializes the project structure. 64 | Logs: 65 | Success: Project created successfully at the specified location. 66 | Error: Target directory already exists or the template directory is missing. 67 | -h, --help 68 | Displays help information about available commands. 69 | 70 | Example: 71 | ```bash 72 | hex --help 73 | ``` 74 | Shows a list of commands and their descriptions. 75 | 76 | ## Project Setup Manual 77 | 78 | Initialize the project by create an entry file in the root of your project (or anywhere) and configure it with your environment setup. environment setup is a folder that keeps your project config for diffrent environments. you can create `default.js, development.js, production.js, test.js` in `/environment` (names are optional) 79 | 80 | Require the package by using the following code to import Hex-Micro in your project: 81 | ```javascript 82 | const path = require('path'); 83 | const { _HEX } = require("hex-micro"); 84 | // Create Hex App instance 85 | const hexApp = new _HEX(path.join(__dirname, './example.domain/environments')); // environment path inside your domain 86 | hexApp.launch(); 87 | ``` 88 | when needed you can stop app by calling this method: 89 | ```javascript 90 | hexApp.stop(); 91 | ``` 92 | 93 | ## Environment Configuration 94 | 95 | The path for environments is critical and contains separate configuration files for different environments such as development, production, test, and default. Each environment follows a similar structure, allowing for tailored configurations. 96 | 97 | ### Example Environment Configuration 98 | ```javascript 99 | const path = require('path'); 100 | // /environment/default.js 101 | // /environment/development.js 102 | // /environment/production.js 103 | // /environment/test.js 104 | module.exports = { 105 | "event": { 106 | "emitter": "eventemitter2" 107 | }, 108 | "packages": [ 109 | 'http', 'jwt' 110 | ], 111 | "commandsPath": [ 112 | path.join(__dirname, "../commands") 113 | ], 114 | "eventsPath": [ 115 | path.join(__dirname, "../events") 116 | ], 117 | "servicesPath": [ 118 | { 119 | path: path.join(__dirname, "../services"), 120 | namespace: "domain.services" 121 | } 122 | ], 123 | "middlewaresPath": [ 124 | path.join(__dirname, "../middlewares") 125 | ], 126 | "database": { 127 | defaultDB: { 128 | type: 'sqlite', 129 | filename: './data/default.db', 130 | initialQuery: [ 131 | `CREATE TABLE IF NOT EXISTS users ( 132 | id TEXT PRIMARY KEY, 133 | name TEXT NOT NULL, 134 | email TEXT NOT NULL 135 | );` 136 | ] 137 | } 138 | }, 139 | "servers": [ // you can add as many you want 140 | { 141 | "name": "MainServer", 142 | "host": "localhost", 143 | "port": 3000, 144 | "type": "http", 145 | "ssl": false 146 | } 147 | ] 148 | }; 149 | ``` 150 | ### Directory Structure 151 | 152 | Hex-Micro uses a structured directory layout for clean separation of concerns: 153 | - Commands: Business logic commands. 154 | - Middlewares: Request and response processing. 155 | - Services: Application-specific service logic. 156 | - Packages: External integrations. 157 | - Repositories: Data management layers. 158 | - Events: Event-driven logic. 159 | 160 | ## Development 161 | ## Examples and Sample Scenarios 162 | 163 | To better understand how to use Hex Micro and explore an scenario, you can refer to the `example` directory in the project. This directory includes two different use cases: 164 | 165 | 1. **Basic Example**: A simple and foundational example to get started with Hex Micro. 166 | 2. **Real-World Scenario**: A comprehensive and practical example demonstrating the advanced capabilities of the framework in a real-world project. 167 | 168 | ### Accessing the Examples 169 | - **GitHub**: Visit the [example](https://github.com/Tariux/hex/tree/main/example) directory in the GitHub repository. 170 | - **Module Folder**: All examples are available in the `example` folder within the `hex-micro` module. 171 | 172 | ### Enhanced Documentation 173 | In the future, the project documentation will be further expanded, and additional content will be added. Be sure to follow the GitHub repository for updates and more details. 174 | 175 | ### Commands 176 | 177 | ### Middlewares 178 | 179 | ### Services 180 | 181 | ### Packages 182 | 183 | ### Events 184 | 185 | ## Links 186 | 187 | - GitHub: [Hex](https://github.com/Tariux/hex) 188 | - NPM: [hex-micro](https://www.npmjs.com/package/hex-micro) 189 | -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { program } = require('commander'); 4 | const { _HEX } = require('..'); 5 | const path = require('path'); 6 | const { existsSync, mkdirSync, copyFileSync, readdirSync, statSync } = require('fs'); 7 | const { tools } = require('../src/utils/ToolManager') || { logger: console }; 8 | 9 | let hexApp; 10 | 11 | function copyDirectory(source, target) { 12 | if (!existsSync(target)) { 13 | mkdirSync(target, { recursive: true }); 14 | } 15 | 16 | const files = readdirSync(source); 17 | 18 | files.forEach((file) => { 19 | const sourceFile = path.join(source, file); 20 | const targetFile = path.join(target, file); 21 | if (statSync(sourceFile).isDirectory()) { 22 | copyDirectory(sourceFile, targetFile); 23 | } else { 24 | copyFileSync(sourceFile, targetFile); 25 | } 26 | }); 27 | } 28 | 29 | program 30 | .version('1.0.0') 31 | .description('A CLI for managing the Hex Micro package') 32 | .addHelpCommand('help', 'Display help for commands') 33 | .on('--help', () => { 34 | console.log('\nExamples:'); 35 | console.log(' $ hex start Start the service with a specified environment'); 36 | console.log(' $ hex create Create a new Hex Micro project'); 37 | }); 38 | 39 | program 40 | .command('hex') 41 | .description('Start the Hex Micro service with the specified environment directory configuration') 42 | .action((env) => { 43 | }) 44 | 45 | program 46 | .command('start ') 47 | .description('Start the Hex Micro service with the specified environment directory configuration') 48 | .action((env) => { 49 | try { 50 | let envPath = path.join(process.cwd(), env); 51 | if (!envPath) { 52 | tools.logger.error('Error: Environment path is required.'); 53 | process.exit(1); 54 | } 55 | 56 | if (existsSync(path.join(envPath, 'environments'))) { 57 | envPath = path.join(envPath, 'environments'); 58 | tools.logger.info('Found project directory with environments folder. Loading from:', envPath); 59 | } 60 | if (!existsSync(envPath)) { 61 | tools.logger.error('Error: Environment path does not exist.'); 62 | process.exit(1); 63 | } 64 | 65 | if ( 66 | !existsSync(path.join(envPath, 'default.js')) && 67 | !existsSync(path.join(envPath, 'production.js')) && 68 | !existsSync(path.join(envPath, 'test.js')) && 69 | !existsSync(path.join(envPath, 'development.js')) 70 | ) { 71 | tools.logger.error('Error: Environment directory must contain configuration files.'); 72 | tools.logger.error('Required: default.js or development.js, production.js, test.js.'); 73 | process.exit(1); 74 | } 75 | 76 | hexApp = new _HEX(envPath); 77 | hexApp.launch() 78 | .then(() => { 79 | tools.logger.info('Service started successfully using environment:', env); 80 | }) 81 | .catch((error) => { 82 | tools.logger.error('Failed to start service:', error.message); 83 | process.exit(1); 84 | }); 85 | } catch (error) { 86 | tools.logger.error('Initialization failed:', error.message); 87 | process.exit(1); 88 | } 89 | }); 90 | 91 | program 92 | .command('create ') 93 | .description('Create a new Hex Micro project at the specified path') 94 | .action((projectPath) => { 95 | try { 96 | const templateDir = path.join(__dirname, '../example'); 97 | const targetDir = path.resolve(process.cwd(), projectPath); 98 | 99 | if (!existsSync(templateDir)) { 100 | tools.logger.error('Error: Template directory does not exist.'); 101 | process.exit(1); 102 | } 103 | 104 | if (existsSync(targetDir)) { 105 | tools.logger.error('Error: Target directory already exists.'); 106 | process.exit(1); 107 | } 108 | 109 | copyDirectory(templateDir, targetDir); 110 | tools.logger.info('Project created successfully at:', targetDir); 111 | } catch (error) { 112 | tools.logger.error('Project creation failed:', error.message); 113 | process.exit(1); 114 | } 115 | }); 116 | 117 | if (!process.argv.slice(2).length) { 118 | program.outputHelp(); 119 | process.exit(0); 120 | } 121 | 122 | program.parse(process.argv); -------------------------------------------------------------------------------- /example/example.domain/commands/Another.command.js: -------------------------------------------------------------------------------- 1 | class AnotherCommand { 2 | static descriptor = { 3 | commandName: 'AnotherCommand', 4 | type: 'REQUEST', 5 | protocol: 'HTTP', 6 | contentType: 'text/json', 7 | routes: [ 8 | { 9 | method: 'GET', 10 | target: '/test', 11 | handler: 'test', 12 | }, 13 | ], 14 | }; 15 | constructor(services) { 16 | } 17 | 18 | async test() { 19 | return { 20 | status: 'success', 21 | message: 'test successfullyasdasdasd', 22 | }; 23 | } 24 | 25 | }; 26 | 27 | 28 | module.exports = AnotherCommand; -------------------------------------------------------------------------------- /example/example.domain/environments/_sample.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "blacklist": { 5 | "routes": [ 6 | { 7 | type: 'REQUEST', 8 | protocol: 'HTTP', 9 | method: 'GET', 10 | target: '/favicon.ico', 11 | } 12 | ], 13 | "ip": [ 14 | 15 | ], 16 | }, 17 | "event": { 18 | "emitter": "eventemitter2" 19 | }, 20 | "packages": [ 21 | 'http' 22 | ], 23 | "commandsPath": [ 24 | path.join(__dirname, "../commands") 25 | ], 26 | "eventsPath": [ 27 | path.join(__dirname, "../events") 28 | ], 29 | "servicesPath": [ 30 | { 31 | path: path.join(__dirname, "../services"), 32 | namespace: "domain.services" 33 | } 34 | ], 35 | "middlewaresPath": [ 36 | path.join(__dirname, "../middlewares") 37 | ], 38 | "database": { 39 | myMongoDB: { 40 | type: 'mongodb', 41 | connectionString: 'mongodb://localhost:27017/mydb', 42 | }, 43 | myRedis: { 44 | type: 'redis', 45 | host: 'localhost', 46 | port: 6379, 47 | }, 48 | mySqlLite1: { 49 | type: 'sqlite', 50 | filename: './storage/db.sqlite1', 51 | }, 52 | mySqlLite2: { 53 | type: 'sqlite', 54 | filename: './storage/db.sqlite2', 55 | initialQuery: [` 56 | CREATE TABLE IF NOT EXISTS users ( 57 | id TEXT PRIMARY KEY, 58 | birthday_yyyy TEXT NOT NULL, 59 | birthday_mm TEXT NOT NULL, 60 | birthday_dd TEXT NOT NULL 61 | ); 62 | `, 63 | ` 64 | CREATE TABLE IF NOT EXISTS profiles ( 65 | userId TEXT PRIMARY KEY, 66 | firstName TEXT NOT NULL, 67 | lastName TEXT NOT NULL, 68 | email TEXT NOT NULL, 69 | FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE 70 | ); 71 | `], 72 | }, 73 | myMySQL: { 74 | type: 'mysql', 75 | host: 'localhost', 76 | user: 'root', 77 | password: '', 78 | database: 'mydb', 79 | }, 80 | 81 | }, 82 | "servers": [ 83 | { 84 | "name": "ServerNumberOne", 85 | "host": "localhost", 86 | "port": 442, 87 | "type": "http", 88 | "ssl": true, 89 | }, 90 | { 91 | "name": "ServerNumberTwo", 92 | "host": "localhost", 93 | "port": 80, 94 | "type": "http", 95 | }, 96 | { 97 | "name": "ServerNumberThree", 98 | "host": "localhost", 99 | "port": 1000, 100 | "type": "http", 101 | }, 102 | { 103 | "name": "ServerHTTP3", 104 | "host": "localhost", 105 | "port": 90, 106 | "type": "quic", 107 | }, 108 | { 109 | "name": "ServerRPC", 110 | "host": "localhost", 111 | "port": 100, 112 | "type": "rpc", 113 | }, 114 | ] 115 | } -------------------------------------------------------------------------------- /example/example.domain/environments/default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "blacklist": { 5 | "routes": [ 6 | { 7 | type: 'REQUEST', 8 | protocol: 'HTTP', 9 | method: 'GET', 10 | target: '/favicon.ico', 11 | } 12 | ], 13 | "ip": [ 14 | 15 | ], 16 | }, 17 | "event": { 18 | "emitter": "eventemitter2" 19 | }, 20 | "commandsPath": [ 21 | path.join(__dirname, "../commands") 22 | ], 23 | "eventsPath": [ 24 | ], 25 | "servicesPath": [ 26 | ], 27 | "middlewaresPath": [ 28 | ], 29 | "database": { 30 | }, 31 | "servers" : [ 32 | { 33 | "name": "serverdump", 34 | "host": "localhost", 35 | "port": 82, 36 | "type": "http", 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /example/example.domain/environments/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/example.domain/environments/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/example.domain/environments/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/helper.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process"); 2 | const fs = require("fs"); 3 | const readline = require("readline"); 4 | 5 | function logToFile(filename, data) { 6 | fs.appendFileSync(filename, data + "\n", "utf8"); 7 | } 8 | 9 | function runExternalProgram(name ,program, args = []) { 10 | console.log(`Starting ${program} with arguments ${args.join(" ")}`); 11 | 12 | const process = spawn(program, args); 13 | 14 | // Reading output line by line 15 | const rl = readline.createInterface({ 16 | input: process.stdout, 17 | terminal: false, 18 | }); 19 | 20 | rl.on("line", (line) => { 21 | console.log(`[${name}] Output: ${line}`); 22 | 23 | // Log lines containing "SPECIAL_IP" to the log file 24 | if (line.includes("SPECIAL_IP")) { 25 | logToFile("special_output.log", line); 26 | } 27 | }); 28 | 29 | // Handle errors 30 | process.stderr.on("data", (data) => { 31 | console.error(`Error: ${data}`); 32 | }); 33 | 34 | // Handle process exit 35 | process.on("close", (code) => { 36 | console.log(`Process exited with code ${code}`); 37 | }); 38 | 39 | // Input handler for user commands 40 | const userInput = readline.createInterface({ 41 | input: process.stdin, 42 | output: process.stdout, 43 | }); 44 | 45 | userInput.on("line", (input) => { 46 | if (input.trim().toUpperCase() === "EXIT") { 47 | console.log("Exiting program."); 48 | process.kill(); 49 | userInput.close(); 50 | } else { 51 | console.log(`Sending command: ${input}`); 52 | process.stdin.write(input + "\n"); 53 | } 54 | }); 55 | 56 | return process; 57 | } 58 | 59 | module.exports = {runExternalProgram} -------------------------------------------------------------------------------- /example/multi-micro.js: -------------------------------------------------------------------------------- 1 | const { runExternalProgram } = require("./helper"); 2 | const path = require('path'); 3 | 4 | runExternalProgram('user.micro', "node", [path.join(__dirname, './user.micro.js')]); 5 | runExternalProgram('test.micro', "node", [path.join(__dirname, './test.micro.js')]); 6 | -------------------------------------------------------------------------------- /example/test.micro.js.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { _HEX } = require(".."); 3 | const hexApp = new _HEX(path.join(__dirname, './example.domain/environments')); 4 | hexApp.launch(); 5 | // hexApp.stop(); -------------------------------------------------------------------------------- /example/user.domain/.env.exmaple: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | SECRET_KEY= -------------------------------------------------------------------------------- /example/user.domain/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#063240", 4 | "titleBar.activeBackground": "#084659", 5 | "titleBar.activeForeground": "#F3FBFE" 6 | }, 7 | "sqltools.connections": [ 8 | { 9 | "previewLimit": 50, 10 | "driver": "SQLite", 11 | "database": "${workspaceFolder:user.domain}/storage/user_db.db", 12 | "name": "user", 13 | "group": "hex" 14 | } 15 | ], 16 | "sqltools.useNodeRuntime": true 17 | } -------------------------------------------------------------------------------- /example/user.domain/commands/Login.command.js: -------------------------------------------------------------------------------- 1 | const { log } = require("util"); 2 | const { tools } = require("../../../src/utils/ToolManager"); 3 | const Auth = require("../models/entities/Auth"); 4 | 5 | class LoginCommand { 6 | static descriptor = { 7 | commandName: 'LoginCommand', 8 | type: 'REQUEST', 9 | protocol: 'HTTP', 10 | loader: ['domain.services.Login'], 11 | contentType: 'text/json', 12 | routes: [ 13 | { 14 | method: 'POST', 15 | target: '/check', 16 | handler: 'check', 17 | }, 18 | { 19 | method: 'POST', 20 | target: '/check', 21 | handler: 'check', 22 | protocol: 'HTTPS', 23 | }, 24 | { 25 | method: 'POST', 26 | target: '/login', 27 | handler: 'login', 28 | }, 29 | { 30 | method: 'POST', 31 | target: '/login', 32 | handler: 'login', 33 | protocol: 'HTTPS', 34 | } 35 | ], 36 | }; 37 | constructor(services) { 38 | this.loginService = services.get('Login'); 39 | } 40 | 41 | async login() { 42 | let validateUser 43 | try { 44 | validateUser = await this.loginService.check(this.command.inputData); 45 | } catch (error) { 46 | tools.logger.error(error) 47 | return { 48 | status: 'fail', 49 | message: 'Login failed', 50 | meta: error.message || error, 51 | }; 52 | } 53 | if (!validateUser) { 54 | return { 55 | status: 'fail', 56 | message: 'Login failed', 57 | }; 58 | } 59 | delete validateUser.errors; 60 | const token = this.command.session.createSession(validateUser, 3600, true); 61 | return { 62 | status: 'success', 63 | token: token || false, 64 | }; 65 | } 66 | 67 | async check() { 68 | try { 69 | const sessions = this.command.session.getSession(true); 70 | console.log('THE SESSION', sessions); 71 | 72 | if (!sessions || !sessions.data) { 73 | return { 74 | status: 'fail', 75 | message: 'session not found', 76 | }; 77 | } 78 | const validateUser = await this.loginService.check({userId: sessions.data.userId, password: sessions.data.password}); 79 | console.log('THE validateUser', validateUser); 80 | if (!validateUser) { 81 | return { 82 | status: 'fail', 83 | message: 'check failed', 84 | }; 85 | } 86 | return { 87 | status: 'success', 88 | }; 89 | } catch (error) { 90 | console.log('error', error); 91 | return { 92 | status: 'fail', 93 | }; 94 | } 95 | 96 | } 97 | }; 98 | 99 | 100 | module.exports = LoginCommand; -------------------------------------------------------------------------------- /example/user.domain/commands/User.command.js: -------------------------------------------------------------------------------- 1 | const UserAggregate = require('../models/aggregates/UserAggregate'); 2 | 3 | class UserCommand { 4 | static descriptor = { 5 | commandName: 'UserCommand', 6 | type: 'REQUEST', 7 | protocol: 'HTTP', 8 | loader: ['domain.services.User'], 9 | contentType: 'text/json', 10 | routes: [ 11 | { 12 | method: 'DELETE', 13 | target: '/user', 14 | handler: 'deleteUser', 15 | }, 16 | { 17 | method: 'POST', 18 | target: '/user', 19 | handler: 'createUser', 20 | }, 21 | { 22 | method: 'PUT', 23 | target: '/user', 24 | handler: 'updateUser', 25 | }, 26 | { 27 | method: 'GET', 28 | target: '/user', 29 | handler: 'getUser', 30 | }, 31 | { 32 | method: 'GET', 33 | target: '/user', 34 | handler: 'getUser', 35 | protocol: 'HTTPS', 36 | }, 37 | { 38 | method: 'GET', 39 | target: '/users', 40 | handler: 'getUsers', 41 | }, 42 | { 43 | method: 'POST', 44 | target: '/users', 45 | handler: 'getUsers', 46 | middlewares: ['UserAccess', 'UserRole'] 47 | }, 48 | { 49 | method: 'GET', 50 | target: '/users', 51 | handler: 'getUsers', 52 | protocol: 'HTTPS', 53 | } 54 | ], 55 | }; 56 | constructor(services) { 57 | this.userService = services.get('User'); 58 | } 59 | 60 | async getUser() { 61 | const { uid } = this.command.queryParams; 62 | const user = await this.userService.get(uid); 63 | if (user) { 64 | return { 65 | status: 'success', 66 | message: 'User retrieved', 67 | data: user, 68 | }; 69 | } 70 | return { 71 | status: 'fail', 72 | message: 'User not found', 73 | }; 74 | 75 | } 76 | 77 | async getUsers() { 78 | return { 79 | status: 'success', 80 | message: 'Users retrieved successfully', 81 | data: await this.userService.getAll(), 82 | }; 83 | } 84 | 85 | async createUser() { 86 | try { 87 | const createUser = await this.userService.create(this.command?.inputData); 88 | if (createUser) { 89 | return { 90 | status: 'success', 91 | message: 'User created successfully', 92 | user: createUser, 93 | }; 94 | } else { 95 | return { 96 | status: 'fail', 97 | message: 'user cannot create', 98 | user: false, 99 | }; 100 | } 101 | } catch (error) { 102 | return { 103 | status: 'fail', 104 | message: 'error while create user', 105 | error: error.message, 106 | }; 107 | 108 | } 109 | 110 | 111 | } 112 | 113 | async updateUser() { 114 | try { 115 | const uid = this.command?.queryParams?.uid; 116 | if (!uid) { 117 | return { 118 | status: 'fail', 119 | message: 'user id not valid ' + uid, 120 | user: false, 121 | }; 122 | } 123 | const updateUser = await this.userService.update(uid, this.command?.inputData); 124 | if (updateUser) { 125 | return { 126 | status: 'success', 127 | message: 'User updated successfully', 128 | user: updateUser, 129 | }; 130 | } else { 131 | return { 132 | status: 'fail', 133 | message: 'user cannot update', 134 | user: false, 135 | }; 136 | } 137 | } catch (error) { 138 | return { 139 | status: 'fail', 140 | message: 'error while update user', 141 | error: error.message, 142 | }; 143 | 144 | } 145 | } 146 | 147 | async deleteUser() { 148 | const { uid } = this.command.queryParams; 149 | const user = await this.userService.get(uid); 150 | if (user) { 151 | return { 152 | status: 'success', 153 | message: 'User deleted successfully', 154 | data: await this.userService.delete(uid), 155 | }; 156 | } 157 | return { 158 | status: 'fail', 159 | message: 'User not found', 160 | }; 161 | } 162 | }; 163 | 164 | 165 | module.exports = UserCommand; -------------------------------------------------------------------------------- /example/user.domain/environments/configs/sqliteIntitalQueryConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | `CREATE TABLE IF NOT EXISTS users ( 3 | id TEXT PRIMARY KEY, 4 | birthday_yyyy INTEGER NOT NULL, 5 | birthday_mm INTEGER NOT NULL, 6 | birthday_dd INTEGER NOT NULL, 7 | createdAt TEXT NOT NULL, 8 | updatedAt TEXT NOT NULL 9 | );` 10 | , 11 | `CREATE TABLE IF NOT EXISTS profiles ( 12 | userId TEXT PRIMARY KEY, 13 | firstName TEXT NOT NULL, 14 | lastName TEXT NOT NULL, 15 | email TEXT NOT NULL, 16 | phoneNumber TEXT NOT NULL, 17 | FOREIGN KEY (userId) REFERENCES users(id) 18 | );` 19 | , 20 | `CREATE TABLE IF NOT EXISTS auth ( 21 | userId TEXT PRIMARY KEY, 22 | password TEXT NOT NULL, 23 | role TEXT NOT NULL CHECK(role IN ('guest', 'user', 'admin', 'moderator')) DEFAULT 'user', 24 | FOREIGN KEY (userId) REFERENCES users(id) 25 | );` 26 | ] -------------------------------------------------------------------------------- /example/user.domain/environments/default.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/user.domain/environments/development.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const sqliteIntitalQueryConfig = require('./configs/sqliteIntitalQueryConfig'); 3 | 4 | module.exports = { 5 | "timeout": 5000, 6 | "blacklist": { 7 | "routes": [ 8 | { 9 | type: 'REQUEST', 10 | protocol: 'HTTP', 11 | method: 'GET', 12 | target: '/favicon.ico', 13 | } 14 | ], 15 | "ip": [ 16 | // soon as possible. 17 | ], 18 | }, 19 | "event": { 20 | "emitter": "eventemitter2" 21 | }, 22 | "packages": [ 23 | { 24 | path:path.join(__dirname, "../packages/hash.js"), 25 | name: 'hash-package' 26 | } 27 | ], 28 | "commandsPath": [ 29 | path.join(__dirname, "../commands") 30 | ], 31 | "eventsPath": [ 32 | path.join(__dirname, "../events") 33 | ], 34 | "servicesPath": [ 35 | { 36 | path: path.join(__dirname, "../services"), 37 | namespace: "domain.services" 38 | } 39 | ], 40 | "middlewaresPath": [ 41 | path.join(__dirname, "../middlewares") 42 | ], 43 | "database": { 44 | user_db: { 45 | type: 'sqlite', 46 | filename: './storage/user_db.db', 47 | initialQuery: sqliteIntitalQueryConfig, 48 | }, 49 | }, 50 | "servers": [ 51 | { 52 | "name": "secure-server", 53 | "host": "localhost", 54 | "port": 3001, 55 | "type": "http", 56 | "ssl": true, 57 | }, 58 | { 59 | "name": "http-server", 60 | "host": "localhost", 61 | "port": 3000, 62 | "type": "http", 63 | }, 64 | { 65 | "name": "mock-http-server", 66 | "host": "localhost", 67 | "port": 8008, 68 | "type": "http", 69 | }, 70 | { 71 | "name": "mock-quic-server", 72 | "host": "localhost", 73 | "port": 2222, 74 | "type": "quic", 75 | }, 76 | ] 77 | } -------------------------------------------------------------------------------- /example/user.domain/environments/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/user.domain/environments/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /example/user.domain/events/UserCreated.event.js: -------------------------------------------------------------------------------- 1 | class UserCreatedEvent { 2 | static eventOptions = { 3 | eventName: 'UserCreatedEvent', 4 | loader: ['domain.services.User'], 5 | }; 6 | 7 | constructor(services) { 8 | this.userService = services.get('User'); 9 | this.orderService = services.get('Order'); 10 | } 11 | 12 | handle (incoming) { 13 | console.log('EVENT CALLED MEOWWW!!!' , incoming); 14 | return 'MEOW'; 15 | } 16 | 17 | }; 18 | 19 | module.exports = UserCreatedEvent; -------------------------------------------------------------------------------- /example/user.domain/middlewares/AdminRole.middleware.js: -------------------------------------------------------------------------------- 1 | class AdminRole { 2 | static options = { 3 | middlewareName: 'AdminRole', 4 | type: 'before', 5 | loader: ['domain.services.Login', 'domain.services.User'], 6 | }; 7 | 8 | constructor(services) { 9 | this.loginService = services.get('Login'); 10 | this.userService = services.get('User'); 11 | } 12 | 13 | async handle(command, next) { 14 | console.log('AdminRole Middleware Called'); 15 | 16 | const sessions = command.data.session.getSession(true); 17 | if (!sessions || !sessions.data) { 18 | throw new Error('session not found'); 19 | } 20 | const user = await this.userService.get(sessions.data.id); 21 | if (!user || !user.auth) { 22 | throw new Error('user not found'); 23 | } 24 | if (user.auth.role === 'admin') { 25 | await next(); 26 | } 27 | throw new Error('access denied, role: ' + user.auth.role); 28 | 29 | } 30 | }; 31 | 32 | 33 | module.exports = AdminRole; -------------------------------------------------------------------------------- /example/user.domain/middlewares/ModeratorRole.middleware.js: -------------------------------------------------------------------------------- 1 | class ModeratorRole { 2 | static options = { 3 | middlewareName: 'ModeratorRole', 4 | type: 'before', 5 | loader: ['domain.services.Login', 'domain.services.User'], 6 | }; 7 | 8 | constructor(services) { 9 | this.loginService = services.get('Login'); 10 | this.userService = services.get('User'); 11 | } 12 | 13 | async handle(command, next) { 14 | console.log('ModeratorRole Middleware Called'); 15 | 16 | const sessions = command.data.session.getSession(true); 17 | if (!sessions || !sessions.data) { 18 | throw new Error('session not found'); 19 | } 20 | const user = await this.userService.get(sessions.data.id); 21 | if (!user || !user.auth) { 22 | throw new Error('user not found'); 23 | } 24 | if (user.auth.role === 'moderator') { 25 | await next(); 26 | } 27 | throw new Error('access denied, role: ' + user.auth.role); 28 | 29 | } 30 | }; 31 | 32 | 33 | module.exports = ModeratorRole; -------------------------------------------------------------------------------- /example/user.domain/middlewares/UserAccess.middleware.js: -------------------------------------------------------------------------------- 1 | class UserAccess { 2 | static options = { 3 | middlewareName: 'UserAccess', 4 | type: 'before', 5 | loader: ['domain.services.Login'], 6 | }; 7 | 8 | constructor(services) { 9 | this.loginService = services.get('Login'); 10 | } 11 | 12 | async handle(command, next) { 13 | console.log('UserAccess Middleware Called'); 14 | 15 | const sessions = command.data.session.getSession(true); 16 | if (!sessions || !sessions.data) { 17 | throw new Error('session not found'); 18 | } 19 | const validateUser = await this.loginService.check({ userId: sessions.data.userId, password: sessions.data.password }); 20 | if (!validateUser) { 21 | throw new Error('check failed'); 22 | } else { 23 | next(); 24 | } 25 | } 26 | }; 27 | 28 | 29 | module.exports = UserAccess; -------------------------------------------------------------------------------- /example/user.domain/middlewares/UserRole.middleware.js: -------------------------------------------------------------------------------- 1 | class UserRole { 2 | static options = { 3 | middlewareName: 'UserRole', 4 | type: 'before', 5 | loader: ['domain.services.Login', 'domain.services.User'], 6 | }; 7 | 8 | constructor(services) { 9 | this.loginService = services.get('Login'); 10 | this.userService = services.get('User'); 11 | } 12 | 13 | async handle(command, next) { 14 | console.log('UserRole Middleware Called'); 15 | 16 | const sessions = command.data.session.getSession(true); 17 | if (!sessions || !sessions.data) { 18 | throw new Error('session not found'); 19 | } 20 | const user = await this.userService.get(sessions.data.id); 21 | if (!user || !user.auth) { 22 | throw new Error('user not found'); 23 | } 24 | if (user.auth.role === 'user') { 25 | await next(); 26 | } 27 | throw new Error('access denied, role: ' + user.auth.role); 28 | 29 | } 30 | }; 31 | 32 | 33 | module.exports = UserRole; -------------------------------------------------------------------------------- /example/user.domain/models/aggregates/UserAggregate.js: -------------------------------------------------------------------------------- 1 | const { _PACKAGES } = require("hex-micro"); 2 | const Auth = require("../entities/Auth"); 3 | const Profile = require("../entities/Profile"); 4 | const Birthday = require("../valueObjects/Birthday"); 5 | 6 | class UserAggregate { 7 | constructor(userId, profile, birthday, auth, metadata) { 8 | this.userId = userId; 9 | this.profile = profile; // Entity 10 | this.birthday = birthday; // Value Object 11 | this.auth = auth; // Entity 12 | this.metadata = metadata; // Value Object (e.g., createdAt, updatedAt) 13 | } 14 | 15 | // Factory method to create a new user 16 | static async create(data) { 17 | let hashedPassword; 18 | try { 19 | const hashPackage = _PACKAGES.getPackage('hash-package'); 20 | hashedPassword = await hashPackage.hashPassword(data.password); 21 | } catch (error) { 22 | console.log('Error while hashing password for: ', data.userId); 23 | console.log(error); 24 | } 25 | const profile = new Profile(data.userId, data.firstName, data.lastName, data.email, data.phoneNumber); 26 | const birthday = new Birthday(data.yyyy, data.mm, data.dd); 27 | const auth = new Auth(data.userId, hashedPassword || data.password); 28 | const metadata = { 29 | createdAt: new Date().toISOString(), 30 | updatedAt: new Date().toISOString(), 31 | }; 32 | 33 | return new UserAggregate(data.userId, profile, birthday, auth, metadata); 34 | } 35 | 36 | // Factory method to create a new user 37 | static async update(oldData, newData) { 38 | const data = {...oldData.profile, ...oldData.birthday, ...oldData.auth, ...newData}; 39 | const profile = new Profile(data.userId, data.firstName, data.lastName, data.email, data.phoneNumber); 40 | const birthday = new Birthday(data.yyyy, data.mm, data.dd); 41 | const auth = new Auth(data.userId, data.password); 42 | const metadata = { 43 | createdAt: new Date().toISOString(), 44 | updatedAt: new Date().toISOString(), 45 | }; 46 | 47 | return new UserAggregate(data.userId, profile, birthday, auth, metadata); 48 | } 49 | 50 | // Update user profile 51 | updateProfile(firstName, lastName, email, phoneNumber) { 52 | this.profile.firstName = firstName; 53 | this.profile.lastName = lastName; 54 | this.profile.updateEmail(email); 55 | this.profile.updatePhoneNumber(phoneNumber); 56 | this.metadata.updatedAt = new Date().toISOString(); 57 | } 58 | 59 | // Update birthday 60 | updateBirthday(yyyy, mm, dd) { 61 | this.birthday = new Birthday(yyyy, mm, dd); 62 | this.metadata.updatedAt = new Date().toISOString(); 63 | } 64 | 65 | // Update password 66 | updatePassword(newPassword) { 67 | this.auth.updatePassword(newPassword); 68 | this.metadata.updatedAt = new Date().toISOString(); 69 | } 70 | } 71 | 72 | module.exports = UserAggregate; -------------------------------------------------------------------------------- /example/user.domain/models/entities/Auth.js: -------------------------------------------------------------------------------- 1 | const {BaseEntity} = require("hex-micro"); 2 | 3 | class Auth extends BaseEntity { 4 | constructor(userId, password) { 5 | super( 6 | { 7 | require: { 8 | userId: 'string', 9 | } 10 | }, 11 | { userId, password } 12 | ); 13 | this.userId = userId; 14 | this.password = password; // In a real app, this should be hashed 15 | } 16 | 17 | updatePassword(newPassword) { 18 | this.password = newPassword; // In a real app, hash the password 19 | } 20 | } 21 | 22 | module.exports = Auth; -------------------------------------------------------------------------------- /example/user.domain/models/entities/Profile.js: -------------------------------------------------------------------------------- 1 | const { BaseEntity } = require("hex-micro"); 2 | const Email = require("../valueObjects/Email"); 3 | const PhoneNumber = require("../valueObjects/PhoneNumber"); 4 | 5 | class Profile extends BaseEntity { 6 | constructor(userId, firstName, lastName, email, phoneNumber) { 7 | super( 8 | { 9 | require: { 10 | userId: 'string', 11 | firstName: 'string', 12 | lastName: 'string', 13 | email: 'string', 14 | phoneNumber: 'string', 15 | } 16 | }, 17 | { userId, firstName, lastName, email, phoneNumber } 18 | ); 19 | this.validate(); 20 | this.email = new Email(this.email).toString(); // Value Object 21 | this.phoneNumber = new PhoneNumber(this.phoneNumber).toString(); // Value Object 22 | } 23 | 24 | updateEmail(newEmail) { 25 | this.email = new Email(newEmail); 26 | } 27 | 28 | updatePhoneNumber(newPhoneNumber) { 29 | this.phoneNumber = new PhoneNumber(newPhoneNumber); 30 | } 31 | } 32 | 33 | module.exports = Profile; 34 | -------------------------------------------------------------------------------- /example/user.domain/models/valueObjects/Birthday.js: -------------------------------------------------------------------------------- 1 | class Birthday { 2 | constructor(yyyy, mm, dd) { 3 | this.yyyy = yyyy; 4 | this.mm = mm; 5 | this.dd = dd; 6 | } 7 | 8 | // Optional: Add validation for the birthday 9 | isValid() { 10 | const date = new Date(this.yyyy, this.mm - 1, this.dd); 11 | return ( 12 | date.getFullYear() === this.yyyy && 13 | date.getMonth() + 1 === this.mm && 14 | date.getDate() === this.dd 15 | ); 16 | } 17 | } 18 | 19 | module.exports = Birthday; -------------------------------------------------------------------------------- /example/user.domain/models/valueObjects/Email.js: -------------------------------------------------------------------------------- 1 | class Email { 2 | constructor(email) { 3 | if (!this.validate(email)) { 4 | throw new Error('Invalid email address: ' + email); 5 | } 6 | this.value = email; 7 | } 8 | 9 | validate(email) { 10 | if (typeof email !== 'string') { 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | toString() { 17 | return this.value; 18 | } 19 | } 20 | 21 | module.exports = Email; -------------------------------------------------------------------------------- /example/user.domain/models/valueObjects/PhoneNumber.js: -------------------------------------------------------------------------------- 1 | class PhoneNumber { 2 | constructor(phoneNumber) { 3 | if (!this.validate(phoneNumber)) { 4 | throw new Error('Invalid phone number: ' + phoneNumber); 5 | } 6 | this.value = phoneNumber; 7 | } 8 | 9 | validate(phoneNumber) { 10 | if (typeof phoneNumber !== 'string') { 11 | return false; 12 | } 13 | return true; 14 | } 15 | 16 | toString() { 17 | return this.value; 18 | } 19 | } 20 | 21 | module.exports = PhoneNumber; -------------------------------------------------------------------------------- /example/user.domain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hex.user.domain", 3 | "version": "1.0.0", 4 | "description": "sample of hex micro package", 5 | "main": "index.js", 6 | "workspaces": [ 7 | "../../" 8 | ], 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "test": "hex start ./" 14 | }, 15 | "keywords": [ 16 | "hex-micro" 17 | ], 18 | "author": "opiumdev ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "hex-micro": "^1.1.1-beta.13", 22 | "node-quic": "^0.1.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/user.domain/packages/hash.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const CryptoJS = require('crypto-js'); 3 | 4 | const SECRET_KEY = process.env.SECRET_KEY || 'MEOWMEOW'; 5 | 6 | async function hashPassword(password) { 7 | const saltRounds = 10; 8 | return await bcrypt.hash(password, saltRounds); 9 | } 10 | 11 | async function validatePassword(password, hash) { 12 | return await bcrypt.compare(password, hash); 13 | } 14 | 15 | function encrypt(data) { 16 | return CryptoJS.AES.encrypt(data, SECRET_KEY).toString(); 17 | } 18 | 19 | function decrypt(encryptedData) { 20 | const bytes = CryptoJS.AES.decrypt(encryptedData, SECRET_KEY); 21 | return bytes.toString(CryptoJS.enc.Utf8); 22 | } 23 | 24 | function isValidHash(hash) { 25 | return /^\$2[aby]\$\d+\$/.test(hash); 26 | } 27 | hash = { 28 | hashPassword, 29 | validatePassword, 30 | encrypt, 31 | decrypt, 32 | isValidHash, 33 | } 34 | module.exports = hash; -------------------------------------------------------------------------------- /example/user.domain/repositories/UserRepository.js: -------------------------------------------------------------------------------- 1 | class UserRepository { 2 | constructor(db) { 3 | this.db = db; 4 | } 5 | 6 | async create(userAggregate) { 7 | const { profile, birthday, auth, metadata } = userAggregate; 8 | const { userId, firstName, lastName, email, phoneNumber } = profile; 9 | const { yyyy, mm, dd } = birthday; 10 | const { password } = auth; 11 | const { createdAt, updatedAt } = metadata; 12 | try { 13 | await this.db.query( 14 | 'INSERT INTO users (id, birthday_yyyy, birthday_mm, birthday_dd, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)', 15 | [userId, yyyy, mm, dd, createdAt, updatedAt] 16 | ); 17 | 18 | await this.db.query( 19 | 'INSERT INTO profiles (userId, firstName, lastName, email, phoneNumber) VALUES (?, ?, ?, ?, ?)', 20 | [userId, firstName, lastName, email.toString(), phoneNumber.toString()] 21 | ); 22 | 23 | await this.db.query( 24 | 'INSERT INTO auth (userId, password) VALUES (?, ?)', 25 | [userId, password] 26 | ); 27 | 28 | return userId; 29 | } catch (error) { 30 | console.error('Error creating user:', error); 31 | throw new Error('error while creating user'); 32 | } 33 | } 34 | 35 | async findProfileByKey(key , value) { 36 | try { 37 | const [user] = await this.db.query( 38 | `SELECT * FROM profiles 39 | JOIN users ON profiles.userId = users.id 40 | JOIN auth ON profiles.userId = auth.userId 41 | WHERE profiles.${key} = ?`, 42 | [value] 43 | ); 44 | 45 | if (!user) return null; 46 | 47 | return { 48 | profile: { 49 | userId: user.id, 50 | firstName: user.firstName, 51 | lastName: user.lastName, 52 | email: user.email, 53 | phoneNumber: user.phoneNumber, 54 | }, 55 | birthday: { 56 | yyyy: user.birthday_yyyy, 57 | mm: user.birthday_mm, 58 | dd: user.birthday_dd, 59 | }, 60 | auth: { 61 | password: user.password, 62 | }, 63 | metadata: { 64 | createdAt: user.createdAt, 65 | updatedAt: user.updatedAt, 66 | }, 67 | }; 68 | } catch (error) { 69 | console.error('Error fetching user:', error); 70 | throw error; 71 | } 72 | } 73 | 74 | async findById(userId) { 75 | try { 76 | const [user] = await this.db.query( 77 | `SELECT 78 | users.id, 79 | users.birthday_yyyy, 80 | users.birthday_mm, 81 | users.birthday_dd, 82 | users.createdAt, 83 | users.updatedAt, 84 | profiles.firstName, 85 | profiles.lastName, 86 | profiles.email, 87 | profiles.phoneNumber, 88 | auth.password, 89 | auth.role 90 | FROM users 91 | JOIN profiles ON users.id = profiles.userId 92 | JOIN auth ON users.id = auth.userId 93 | WHERE users.id = ?`, 94 | [userId] 95 | ); 96 | 97 | if (!user) return null; 98 | 99 | return { 100 | profile: { 101 | userId: user.id, 102 | firstName: user.firstName, 103 | lastName: user.lastName, 104 | email: user.email, 105 | phoneNumber: user.phoneNumber, 106 | }, 107 | birthday: { 108 | yyyy: user.birthday_yyyy, 109 | mm: user.birthday_mm, 110 | dd: user.birthday_dd, 111 | }, 112 | auth: { 113 | password: user.password, 114 | role: user.role, 115 | }, 116 | metadata: { 117 | createdAt: user.createdAt, 118 | updatedAt: user.updatedAt, 119 | }, 120 | }; 121 | } catch (error) { 122 | console.error('Error fetching user:', error); 123 | throw error; 124 | } 125 | } 126 | 127 | async getAll() { 128 | try { 129 | const users = await this.db.query( 130 | `SELECT * 131 | FROM users 132 | JOIN profiles ON users.id = profiles.userId 133 | JOIN auth ON users.id = auth.userId` 134 | ); 135 | 136 | return users.map((user) => ({ 137 | profile: { 138 | userId: user.id, 139 | firstName: user.firstName, 140 | lastName: user.lastName, 141 | email: user.email, 142 | phoneNumber: user.phoneNumber, 143 | }, 144 | birthday: { 145 | yyyy: user.birthday_yyyy, 146 | mm: user.birthday_mm, 147 | dd: user.birthday_dd, 148 | }, 149 | auth: { 150 | password: user.password, 151 | }, 152 | metadata: { 153 | createdAt: user.createdAt, 154 | updatedAt: user.updatedAt, 155 | }, 156 | })); 157 | } catch (error) { 158 | console.error('Error fetching all users:', error); 159 | throw error; 160 | } 161 | } 162 | 163 | async update(userAggregate) { 164 | const { profile, birthday, auth, metadata } = userAggregate; 165 | const { userId, firstName, lastName, email, phoneNumber } = profile; 166 | const { yyyy, mm, dd } = birthday; 167 | const { password } = auth; 168 | const { updatedAt } = metadata; 169 | 170 | try { 171 | await this.db.query( 172 | 'UPDATE users SET birthday_yyyy = ?, birthday_mm = ?, birthday_dd = ?, updatedAt = ? WHERE id = ?', 173 | [yyyy, mm, dd, updatedAt, userId] 174 | ); 175 | 176 | await this.db.query( 177 | 'UPDATE profiles SET firstName = ?, lastName = ?, email = ?, phoneNumber = ? WHERE userId = ?', 178 | [firstName, lastName, email, phoneNumber, userId] 179 | ); 180 | 181 | await this.db.query( 182 | 'UPDATE auth SET password = ? WHERE userId = ?', 183 | [password, userId] 184 | ); 185 | 186 | return true; 187 | } catch (error) { 188 | console.error('Error updating user:', error); 189 | throw error; 190 | } 191 | } 192 | 193 | async delete(userId) { 194 | try { 195 | await this.db.query('DELETE FROM users WHERE id = ?', [userId]); 196 | await this.db.query('DELETE FROM profiles WHERE userId = ?', [userId]); 197 | await this.db.query('DELETE FROM auth WHERE userId = ?', [userId]); 198 | console.log('User deleted successfully'); 199 | return true; 200 | } catch (error) { 201 | console.error('Error deleting user:', error); 202 | throw error; 203 | } 204 | } 205 | } 206 | 207 | module.exports = UserRepository; -------------------------------------------------------------------------------- /example/user.domain/services/LoginService.js: -------------------------------------------------------------------------------- 1 | const { _DB, _PACKAGES } = require("hex-micro"); 2 | const UserRepository = require("../repositories/UserRepository"); 3 | const Auth = require("../models/entities/Auth"); 4 | 5 | class LoginService { 6 | key = 'Login'; 7 | constructor() { 8 | this.#init(); 9 | } 10 | 11 | async #init() { 12 | this.db = await _DB.adapter.getConnection('user_db'); 13 | this.hash = await _PACKAGES.getPackage('hash-package'); 14 | this.userRepository = new UserRepository(this.db); 15 | } 16 | 17 | 18 | async check(inputData) { 19 | let user; 20 | try { 21 | user = await this.get(inputData); 22 | if (!user || !user.data || !user.input?.password) { 23 | return false; 24 | } 25 | } catch (error) { 26 | throw new Error('user cannot be found ', error.message); 27 | } 28 | try { 29 | const inputPassword = user.input.password; 30 | const foundPassword = user.data.auth.password; 31 | const validate = await this.hash.validatePassword(inputPassword, foundPassword); 32 | if (validate) { 33 | return {...user.input, id: user.data.profile.userId}; 34 | } 35 | } catch (error) { 36 | throw new Error('invalid password'); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | 43 | async get(inputData) { 44 | let authEntity; 45 | try { 46 | authEntity = new Auth(inputData.userId, inputData.password); 47 | authEntity.validate(); 48 | } catch (error) { 49 | throw new Error('invalid input data for user: ' + error.message); 50 | } 51 | const foundData = await this.userRepository.findProfileByKey('email', authEntity.userId); 52 | try { 53 | return { 54 | input: authEntity, 55 | data: foundData 56 | }; 57 | } catch (error) { 58 | throw new Error('user cannot be found ' + error.message); 59 | } 60 | } 61 | 62 | } 63 | 64 | module.exports = LoginService; -------------------------------------------------------------------------------- /example/user.domain/services/UserService.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid'); 2 | const { _DB } = require("hex-micro"); 3 | const UserRepository = require("../repositories/UserRepository"); 4 | const UserAggregate = require('../models/aggregates/UserAggregate'); 5 | 6 | class UserService { 7 | key = 'User'; 8 | constructor() { 9 | this.#init(); 10 | } 11 | 12 | async #init() { 13 | this.db = await _DB.adapter.getConnection('user_db'); 14 | this.userRepository = new UserRepository(this.db); 15 | } 16 | 17 | async create(data) { 18 | if (!data) { 19 | return; 20 | } 21 | let userAggregate; 22 | try { 23 | const uuid = uuidv4(); 24 | userAggregate = await UserAggregate.create({userId: uuid, ...data}) 25 | 26 | } catch (error) { 27 | throw new Error('invalid input data for user: ' + error.message); 28 | } 29 | try { 30 | return this.userRepository.create(userAggregate); 31 | } catch (error) { 32 | throw new Error(error.message) 33 | } 34 | } 35 | 36 | async delete(userID) { 37 | return this.userRepository.delete(userID); 38 | } 39 | 40 | async update(userId, data) { 41 | if (!data) { 42 | return; 43 | } 44 | delete data.phoneNumber; 45 | delete data.email; 46 | delete data.password; 47 | let userAggregate; 48 | try { 49 | const userData = await this.userRepository.findById(userId); 50 | if (!userData) { 51 | throw new Error(userId + ' user does not exists') 52 | } 53 | userAggregate = await UserAggregate.update(userData, data) 54 | } catch (error) { 55 | throw new Error('invalid input data for user: ' + error.message); 56 | } 57 | try { 58 | return this.userRepository.update(userAggregate); 59 | } catch (error) { 60 | throw new Error(error.message) 61 | } 62 | } 63 | 64 | async get(userID) { 65 | return this.userRepository.findById(userID); 66 | } 67 | 68 | async getAll() { 69 | return this.userRepository.getAll(); 70 | } 71 | } 72 | 73 | module.exports = UserService; -------------------------------------------------------------------------------- /example/user.domain/test/crud.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { expect } = require("chai"); 3 | const { _HEX } = require("hex-micro"); 4 | const path = require('path'); 5 | 6 | const cl = console; 7 | console = { 8 | log: () => { 9 | 10 | } 11 | } 12 | describe("User CRUD API Test Scenario", function () { 13 | let userId; 14 | 15 | this.timeout(5000); 16 | before(async function () { 17 | this.Hex = new _HEX(path.join(__dirname, '../environments')); 18 | await this.Hex.launch(); 19 | }); 20 | 21 | after(async function () { 22 | await this.Hex.stop(); 23 | }); 24 | 25 | let random = Math.random() * 1000000; 26 | random = Math.round(random) 27 | 28 | it("should create a user and retrieve userId", async function () { 29 | const createUserData = { 30 | firstName: 'Name ' + random, 31 | lastName: 'Last ' + random, 32 | email: `EmailTest${random}@mail.com`, 33 | phoneNumber: '09876' + random, 34 | password: '12345678', 35 | yyyy: '2025', 36 | mm: '01', 37 | dd: '01' 38 | }; 39 | const response = await axios.post('http://localhost:3000/user', createUserData); 40 | expect(response.status).to.equal(200); 41 | expect(response.data.status).to.equal('success'); 42 | userId = response.data.user; 43 | cl.log(' random identety:', random); // 44 | cl.log(' userId:', userId); // 45 | }); 46 | 47 | // it("should update the user", async function () { 48 | // const updateUserData = { 49 | // firstName: 'Updated Name ' + Math.round(random), 50 | // lastName: 'Updated Last ' + Math.round(random), 51 | // email: `EmailTest${random}@mail.com`, 52 | // phoneNumber: '09876' + Math.round(random), 53 | // }; 54 | // const response = await axios.put(`http://localhost:3000/user?uid=${userId}`, updateUserData); 55 | // expect(response.status).to.equal(200); 56 | // expect(response.data.status).to.equal('success'); 57 | // }); 58 | 59 | it("should get the user", async function () { 60 | const response = await axios.get(`http://localhost:3000/user?uid=${userId}`); 61 | expect(response.status).to.equal(200); // 62 | expect(response.data.status).to.equal('success'); 63 | }); 64 | 65 | it("should fail get all users (should be login)", async function () { 66 | const response = await axios.post('http://localhost:3000/users'); 67 | expect(response.data.status).to.not.equal('success'); 68 | // expect(response.data.status).to.equal('success'); 69 | 70 | }); 71 | 72 | it("should delete the user", async function () { 73 | const response = await axios.delete(`http://localhost:3000/user?uid=${userId}`); 74 | expect(response.data.status).to.equal('success'); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /example/user.domain/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login Test Page 7 | 71 | 72 | 73 |
74 | 75 | 80 | 81 | 82 | 86 | 87 |
88 |
89 | 90 | 91 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /example/user.domain/test/login.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { expect } = require("chai"); 3 | const { _HEX } = require("hex-micro"); 4 | const path = require('path'); 5 | 6 | const cl = console; 7 | // console = { 8 | // log: () => { 9 | 10 | // } 11 | // } 12 | describe("User Login API Test Scenario", function () { 13 | let loginResponse; // Store the login response for later inspection 14 | let cookies; // Store cookies for session persistence 15 | 16 | this.timeout(10000); 17 | 18 | before(async function () { 19 | this.Hex = new _HEX(path.join(__dirname, '../environments')); 20 | await this.Hex.launch(); 21 | }); 22 | 23 | after(async function () { 24 | await this.Hex.stop(); 25 | }); 26 | 27 | it("should login", async function () { 28 | const loginData = { 29 | userId: `EmailTest156848@mail.com`, 30 | password: '12345678', 31 | }; 32 | 33 | loginResponse = await axios.post('http://localhost:3000/login', loginData); 34 | expect(loginResponse.status).to.equal(200); 35 | expect(loginResponse.data.status).to.equal('success'); 36 | cookies = loginResponse.headers['set-cookie']; 37 | console.log('Login successful. Cookies:', cookies); 38 | }); 39 | 40 | it("should check session", async function () { 41 | if (!cookies) { 42 | throw new Error('No cookies found. Login might have failed.'); 43 | } 44 | // const fakeCookie = 'sessionId=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiNDY5NTNjMzktODQ3MC00ZjFhLTg0NjEtZGE3ZmYwNTEwN2EwIiwiZXhwIjoxNzM2MjgzMDMxLCJpYXQiOjE3MzYyNzk0MzF9.tdZEOx0pm2RhGZ91QsjlvueiRjKLVcdyJ8G4QgOSiYI; Path=/; HttpOnly; SameSite=strict'; 45 | const checkResponse = await axios.post('http://localhost:3000/check', {}, { 46 | headers: { 47 | Cookie: cookies.join('; ') 48 | // Cookie: fakeCookie 49 | } 50 | }); 51 | expect(checkResponse.status).to.equal(200); 52 | expect(checkResponse.data.status).to.equal('success'); 53 | console.log('Check successful. Cookies:', checkResponse.data); 54 | 55 | }); 56 | 57 | it("should get all users (need to be login)", async function () { 58 | await new Promise((resolve, reject) => { 59 | setTimeout(async () => { 60 | resolve(true) 61 | }, 5000); 62 | 63 | }); 64 | const response = await axios.post('http://localhost:3000/users', {}, { 65 | headers: { 66 | Cookie: cookies.join('; ') 67 | } 68 | }); 69 | console.log('Get successful. Cookies:', response.data.data.length); 70 | expect(response.data.status).to.equal('success'); 71 | 72 | }); 73 | 74 | 75 | // afterEach(function () { 76 | // // Log session and cookies after each test 77 | // if (loginResponse) { 78 | // console.log('Cookies:', loginResponse.headers['set-cookie']); 79 | // } 80 | // }); 81 | }); -------------------------------------------------------------------------------- /example/user.micro.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { _HEX } = require(".."); 3 | const hexApp = new _HEX(path.join(__dirname, './app.user.managment/environments')); 4 | hexApp.launch(); 5 | // hexApp.stop(); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const hex = require("./src/main"); 2 | const Database = require("./src/application/database/Database"); 3 | const Events = require("./src/application/events/Events"); 4 | const Loader = require("./src/application/loader/Loader"); 5 | const PackageManager = require("./src/application/loader/Packages"); 6 | const BaseEntity = require("./src/application/extends/BaseEntity"); 7 | 8 | 9 | exports.BaseEntity = BaseEntity; 10 | exports._HEX = hex; 11 | exports._PACKAGES = PackageManager; 12 | exports._LOADER = Loader; 13 | exports._DB = Database; 14 | exports._EVENT = Events; 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hex-micro", 3 | "version": "1.1.1-beta.15", 4 | "description": "A DDD Hexagonal Architecture for Flexible Web Applications", 5 | "main": "index.js", 6 | "author": "opium66", 7 | "scripts": { 8 | "debug": "node ./example/debug.js", 9 | "test": "mocha test/**" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Tariux/HEX.git" 14 | }, 15 | "exports": { 16 | ".": { 17 | "require": "./index.js" 18 | } 19 | }, 20 | "bin": { 21 | "hex": "./cli/index.js" 22 | }, 23 | "keywords": [ 24 | "domain-driven-design", 25 | "microservice", 26 | "hexagon", 27 | "backend", 28 | "hex-micro" 29 | ], 30 | "license": "MIT", 31 | "dependencies": { 32 | "amqplib": "^0.10.5", 33 | "axios": "^1.7.9", 34 | "bcrypt": "^5.1.1", 35 | "chai": "^4.5.0", 36 | "cli-table": "^0.3.11", 37 | "commander": "^13.0.0", 38 | "crypto-js": "^4.2.0", 39 | "dotenv": "^16.4.7", 40 | "eventemitter2": "^6.4.9", 41 | "express": "^4.21.2", 42 | "http3": "^0.0.1", 43 | "jsonwebtoken": "^9.0.2", 44 | "kafkajs": "^2.2.4", 45 | "mocha": "^11.0.1", 46 | "mongodb": "^6.12.0", 47 | "mysql2": "^3.12.0", 48 | "net": "^1.0.2", 49 | "node-quic": "^0.1.3", 50 | "nodejs": "^0.0.0", 51 | "redis": "^4.7.0", 52 | "sqlite3": "^5.1.7", 53 | "url": "^0.11.4", 54 | "util": "^0.12.5", 55 | "uuid": "^11.0.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /shared/utils/generateCertificates.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exec } = require('child_process'); 3 | const path = require('path'); 4 | 5 | 6 | async function checkCertificate(keyPath) { 7 | let maxAttempts = 5; 8 | 9 | return new Promise((resolve, reject) => { 10 | const interval = setInterval(() => { 11 | if (maxAttempts <= 0) { 12 | clearInterval(interval); // Stop checking when max attempts are exhausted 13 | reject(false); // Reject the promise 14 | } 15 | 16 | try { 17 | // Check if file exists and is not empty 18 | const check = fs.readFileSync(keyPath, 'utf8'); 19 | if (check && check !== '') { 20 | clearInterval(interval); // Stop checking as we've found the file 21 | resolve(true); // Resolve the promise 22 | } 23 | } catch (error) { 24 | // If the file doesn't exist or other error, do nothing and keep trying 25 | } 26 | 27 | maxAttempts--; // Decrement attempts 28 | }, 500); // Check every 500ms 29 | }); 30 | } 31 | 32 | async function generateCertificates(keyPath, certPath) { 33 | // Ensure the directory exists (create it recursively if needed) 34 | const keyDir = path.dirname(keyPath); 35 | const certDir = path.dirname(certPath); 36 | 37 | // Create directories if they don't exist 38 | if (!fs.existsSync(keyDir)) { 39 | fs.mkdirSync(keyDir, { recursive: true }); 40 | } 41 | 42 | if (!fs.existsSync(certDir)) { 43 | fs.mkdirSync(certDir, { recursive: true }); 44 | } 45 | 46 | // Check if the files already exist 47 | if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { 48 | return true; 49 | } 50 | 51 | // Create an OpenSSL configuration file with the necessary details 52 | const configPath = path.join(keyDir, 'openssl.cnf'); 53 | 54 | // OpenSSL config template with subjectAltName for localhost and 127.0.0.1 55 | const configContent = ` 56 | [ req ] 57 | default_bits = 2048 58 | default_keyfile = ${keyPath} 59 | distinguished_name = req_distinguished_name 60 | x509_extensions = v3_ca 61 | 62 | [ req_distinguished_name ] 63 | commonName = Common Name (e.g. server FQDN or YOUR name) 64 | commonName_max = 64 65 | 66 | [ v3_ca ] 67 | subjectAltName = @alt_names 68 | 69 | [ alt_names ] 70 | DNS.1 = localhost 71 | IP.1 = 127.0.0.1 72 | `; 73 | 74 | // Write the configuration file 75 | fs.writeFileSync(configPath, configContent); 76 | 77 | // Command to generate the SSL certificate 78 | const opensslCommand = `openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout ${keyPath} -out ${certPath} -subj "/CN=localhost" -config ${configPath}`; 79 | 80 | exec(opensslCommand, (error, stdout, stderr) => { 81 | if (error) { 82 | console.error(`Error generating certificates: ${stderr}`); 83 | return; 84 | } 85 | fs.unlinkSync(configPath); 86 | }); 87 | 88 | if (await checkCertificate(keyPath)) { 89 | return true; 90 | } else { 91 | return false; 92 | } 93 | 94 | } 95 | 96 | module.exports = generateCertificates; 97 | -------------------------------------------------------------------------------- /src/adapters/BaseLauncher.js: -------------------------------------------------------------------------------- 1 | const { tools } = require("../utils/ToolManager"); 2 | 3 | class BaseLauncher { 4 | constructor(name) { 5 | this.name = name; 6 | } 7 | 8 | /** 9 | * Starts the service. 10 | * Must be implemented by subclasses. 11 | */ 12 | start() { 13 | if (!this.servers) { 14 | tools.logger.error('define servers for launcher', this.name); 15 | } 16 | this.servers.forEach((instance, key) => { 17 | try { 18 | instance.listen().then(() => { 19 | tools.logger.info(`${key} server is running: http${instance?.ssl ? 's' : ''}://${instance?.host || 'Uknown'}:${instance?.port || 'NaN'}`); 20 | instance.updateStatus(true); 21 | 22 | }); 23 | } catch (error) { 24 | tools.logger.error(`${key} server failed`); 25 | tools.logger.error(error); 26 | } 27 | }); 28 | } 29 | 30 | /** 31 | * Stops the service (optional). 32 | * Can be overridden by subclasses if needed. 33 | */ 34 | stop() { 35 | if (!this.servers) { 36 | tools.logger.error('define servers for launcher', this.name); 37 | } 38 | this.servers.forEach((instance, key) => { 39 | try { 40 | instance.stop().then(() => { 41 | tools.logger.info(`${key} server is stopped`); 42 | instance.updateStatus(false); 43 | }); 44 | } catch (error) { 45 | tools.logger.error(`${key} server stop failed`); 46 | tools.logger.error(error); 47 | } 48 | }); 49 | } 50 | 51 | getServers() { 52 | return this.servers || false; 53 | } 54 | 55 | } 56 | 57 | module.exports = BaseLauncher; 58 | -------------------------------------------------------------------------------- /src/adapters/BaseServer.js: -------------------------------------------------------------------------------- 1 | const Command = require("../application/command/Command"); 2 | const EventManager = require("../application/events/EventManager"); 3 | const ConfigCenter = require("../config/ConfigCenter"); 4 | const SessionManager = require("../utils/SessionManager"); 5 | const { tools } = require("../utils/ToolManager"); 6 | 7 | class BaseServer { 8 | status = false; 9 | 10 | constructor(config) { 11 | if (!this.#validateServerConfig(config)) { 12 | return; 13 | } 14 | this.port = config.port; 15 | this.host = config.host; 16 | this.ssl = config.ssl; 17 | this.emitter = EventManager.getInstance().emitter; 18 | this.timeout = ConfigCenter.getInstance().get('timeout') || 2000; 19 | this.#initWhitelist(); 20 | } 21 | 22 | #initWhitelist() { 23 | this.blacklistConfig = ConfigCenter.getInstance().get('blacklist') || false; 24 | if (!this.blacklistConfig) { 25 | return; 26 | } 27 | if (typeof this.blacklistConfig?.routes !== 'object') { 28 | return; 29 | } 30 | this.blacklistPatterns = new Set(); 31 | this.blacklistConfig.routes.forEach(exclude => { 32 | const blacklistPattern = Command.pattern(exclude); 33 | this.blacklistPatterns.add(blacklistPattern) 34 | }); 35 | } 36 | 37 | #validateServerConfig(server) { 38 | return ( 39 | server && server?.host && server?.port && server?.type && server?.name && 40 | typeof server?.port === 'number' 41 | ) 42 | } 43 | 44 | handleIncomingRequest(request, response) { 45 | const command = new Command(request); 46 | const requestPattern = command.pattern(); 47 | const responsePattern = `${requestPattern}:RESPONSE`; 48 | command.setSession(new SessionManager(request.data, response)) 49 | 50 | if (this.blacklistPatterns && this.blacklistPatterns.has(requestPattern)) { 51 | tools.logger.warn(`[REJECT]: new command ${requestPattern} at ${new Date().getTime()}`); 52 | return new Promise((resolve, reject) => { 53 | reject('blacklist'); 54 | }); 55 | } 56 | 57 | const incoming = new Promise((resolve, reject) => { 58 | this.emitter.subscribe(responsePattern, (command) => { 59 | clearTimeout(timeout) 60 | resolve(command); 61 | }); 62 | const timeout = setTimeout(() => { 63 | tools.logger.info(`[TIMEOUT]: new command ${requestPattern} at ${new Date().getTime()}`); 64 | reject('timeout'); 65 | }, this.timeout); 66 | }) 67 | .catch((error) => { 68 | tools.logger.error(`[ERROR]: new command ${requestPattern} at ${new Date().getTime()}`); 69 | tools.logger.error(error); 70 | return new Promise((resolve, reject) => { 71 | reject('blacklist'); 72 | }); 73 | }) 74 | 75 | tools.logger.info(`[OK]: new command ${requestPattern} at ${new Date().getTime()}`); 76 | this.emitter.publish(requestPattern, command); 77 | return incoming; 78 | 79 | } 80 | 81 | listen() { 82 | throw new Error(`Listen method not implemented in ${this.constructor.name}`); 83 | } 84 | 85 | stop() { 86 | throw new Error(`Stop method not implemented in ${this.constructor.name}`); 87 | } 88 | 89 | updateStatus(status) { 90 | this.status = status; 91 | } 92 | 93 | getStatus() { 94 | return this.status; 95 | } 96 | 97 | } 98 | 99 | module.exports = BaseServer; 100 | -------------------------------------------------------------------------------- /src/adapters/MainLauncher.js: -------------------------------------------------------------------------------- 1 | const HttpLauncher = require('../adapters/http/HttpLauncher'); 2 | const RpcLauncher = require('../adapters/rpc/RpcLauncher'); 3 | const ConfigCenter = require('../config/ConfigCenter'); 4 | const { tools } = require('../utils/ToolManager'); 5 | 6 | class MainLauncher { 7 | launchers = [] 8 | constructor() { 9 | this.config = ConfigCenter.getInstance().get('servers'); 10 | this.servers = tools.helper.groupBy(Object.values(this.config), 'type'); 11 | if (this.servers.http) { 12 | this.launchers.push(new HttpLauncher([...this.servers.http || [], ...this.servers.quic || []])) 13 | } 14 | if (this.servers.rpc) { 15 | this.launchers.push(new RpcLauncher(this.servers.rpc)) 16 | } 17 | if (this.servers.quic) { 18 | // this.launchers.push(new QuicLauncher(this.servers.quic)) 19 | } 20 | } 21 | 22 | start() { 23 | return Promise.all(this.launchers.map((launcher) => launcher.start())).then(() => { 24 | tools.logger.info('launchers are running.'); 25 | }); 26 | } 27 | 28 | /** 29 | * Stops all launchers in sequence. 30 | */ 31 | stop() { 32 | return Promise.all(this.launchers.map((launcher) => launcher.stop())).then(() => { 33 | tools.logger.info('launchers are stopped.'); 34 | // process.exit(1); 35 | return; 36 | }); 37 | } 38 | } 39 | 40 | module.exports = MainLauncher; 41 | -------------------------------------------------------------------------------- /src/adapters/http/Http2Server.js: -------------------------------------------------------------------------------- 1 | const http2 = require('http2'); 2 | const fs = require('fs'); 3 | const url = require('url'); 4 | const ConfigCenter = require('../../config/ConfigCenter'); 5 | const generateCertificates = require('../../../shared/utils/generateCertificates'); 6 | const BaseServer = require('../BaseServer'); 7 | const { tools } = require('../../utils/ToolManager'); 8 | 9 | class Http2Server extends BaseServer { 10 | constructor(config) { 11 | super(config); 12 | this.credentials = ConfigCenter.getInstance().get('credentials'); 13 | this.port = (typeof config.ssl === 'number') ? config.ssl : config.port + 1; 14 | } 15 | 16 | #generateCertificates() { 17 | const cert = generateCertificates(this.credentials.keyPath, this.credentials.certPath); 18 | if (!cert) { 19 | tools.logger.error('SSL: Failed to generate certificates'); 20 | throw new Error('Certificate generation failed'); 21 | } 22 | tools.logger.info('SSL: Certificates generated successfully'); 23 | return cert; 24 | } 25 | 26 | #createServerOptions() { 27 | return { 28 | key: fs.readFileSync(this.credentials.keyPath), 29 | cert: fs.readFileSync(this.credentials.certPath), 30 | }; 31 | } 32 | 33 | #parseQueryParams(target) { 34 | const parsedUrl = url.parse(target, true); 35 | return parsedUrl.query; 36 | } 37 | 38 | listen() { 39 | try { 40 | this.#generateCertificates(); 41 | const serverOptions = this.#createServerOptions(); 42 | 43 | this.app = http2.createSecureServer(serverOptions, (req, res) => { 44 | let body = ''; 45 | const queryParams = this.#parseQueryParams(req.url); 46 | 47 | req.on('data', (chunk) => { 48 | body += chunk.toString(); 49 | }); 50 | 51 | req.on('end', () => { 52 | let inputData = null; 53 | if (req.headers['content-type'] === 'application/json') { 54 | try { 55 | inputData = JSON.parse(body); 56 | } catch (error) { 57 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 58 | res.end('Invalid JSON format'); 59 | return; 60 | } 61 | } 62 | 63 | this.handleIncomingRequest({ type: 'HTTPS', data: req, inputData, queryParams }, res) 64 | .then((command) => { 65 | const contentType = command?.dispatcher?.contentType || 'text/plain'; 66 | res.writeHead(command?.statusCode || 400, { 'Content-Type': contentType }); 67 | switch (contentType) { 68 | case 'text/json': 69 | res.end(JSON.stringify(command.response)); 70 | break; 71 | default: 72 | res.end(command.response.toString()); 73 | break; 74 | } 75 | }) 76 | .catch((error) => { 77 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 78 | res.end(error.toString()); 79 | }); 80 | }); 81 | 82 | req.on('error', (error) => { 83 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 84 | res.end(`Request error: ${error.message}`); 85 | }); 86 | }); 87 | 88 | return new Promise((resolve) => { 89 | this.server = this.app.listen(this.port, () => { 90 | resolve(); 91 | }); 92 | }); 93 | } catch (error) { 94 | tools.logger.error(`Error starting HTTP/2 server: ${error.message}`); 95 | throw error; 96 | } 97 | } 98 | 99 | stop() { 100 | return new Promise((resolve, reject) => { 101 | this.server.close((err) => { 102 | if (err) { 103 | tools.logger.error(`Error stopping HTTP/2 server: ${err.message}`); 104 | reject(err); 105 | } else { 106 | resolve(); 107 | } 108 | }); 109 | }); 110 | } 111 | } 112 | 113 | module.exports = Http2Server; 114 | -------------------------------------------------------------------------------- /src/adapters/http/Http3Server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const url = require('url'); 3 | const nghttp3 = require('nghttp3'); 4 | const ConfigCenter = require('../../config/ConfigCenter'); 5 | const generateCertificates = require('../../../shared/utils/generateCertificates'); 6 | const BaseServer = require('../BaseServer'); 7 | const { tools } = require('../../utils/ToolManager'); 8 | 9 | class Http3Server extends BaseServer { 10 | constructor(config) { 11 | super(config); 12 | this.credentials = ConfigCenter.getInstance().get('credentials'); 13 | this.port = typeof config.ssl === 'number' ? config.ssl : (config.port || 443) + 1; 14 | this.app = null; 15 | this.server = null; 16 | } 17 | 18 | #generateCertificates() { 19 | try { 20 | const cert = generateCertificates(this.credentials.keyPath, this.credentials.certPath); 21 | if (!cert) { 22 | throw new Error('Certificate generation failed'); 23 | } 24 | tools.logger.info('SSL: Certificates generated successfully'); 25 | return cert; 26 | } catch (error) { 27 | tools.logger.error(`SSL: ${error.message}`); 28 | throw error; 29 | } 30 | } 31 | 32 | #createServerOptions() { 33 | try { 34 | return { 35 | key: fs.readFileSync(this.credentials.keyPath), 36 | cert: fs.readFileSync(this.credentials.certPath), 37 | allowHTTP1: true, 38 | }; 39 | } catch (error) { 40 | tools.logger.error(`Error reading SSL files: ${error.message}`); 41 | throw error; 42 | } 43 | } 44 | 45 | #parseQueryParams(target) { 46 | try { 47 | const parsedUrl = url.parse(target, true); 48 | return parsedUrl.query; 49 | } catch (error) { 50 | tools.logger.error(`Error parsing query parameters: ${error.message}`); 51 | return {}; 52 | } 53 | } 54 | 55 | #handleStream(stream, headers) { 56 | let body = ''; 57 | 58 | stream.on('data', (chunk) => { 59 | body += chunk.toString(); 60 | }); 61 | 62 | stream.on('end', async () => { 63 | try { 64 | const queryParams = this.#parseQueryParams(headers[':path'] || ''); 65 | let inputData = null; 66 | 67 | if (headers['content-type'] === 'application/json') { 68 | try { 69 | inputData = JSON.parse(body); 70 | } catch { 71 | this.#sendResponse(stream, 400, 'text/plain', 'Invalid JSON format'); 72 | return; 73 | } 74 | } 75 | 76 | const command = await this.handleIncomingRequest({ 77 | type: 'HTTP3', 78 | data: { headers }, 79 | inputData, 80 | queryParams, 81 | }); 82 | 83 | const contentType = command?.dispatcher?.contentType || 'text/plain'; 84 | this.#sendResponse(stream, command?.statusCode || 200, contentType, command.response); 85 | } catch (error) { 86 | tools.logger.error(`Error processing stream: ${error.message}`); 87 | this.#sendResponse(stream, 500, 'text/plain', `Server error: ${error.message}`); 88 | } 89 | }); 90 | 91 | stream.on('error', (error) => { 92 | tools.logger.error(`Stream error: ${error.message}`); 93 | this.#sendResponse(stream, 500, 'text/plain', `Stream error: ${error.message}`); 94 | }); 95 | } 96 | 97 | #sendResponse(stream, statusCode, contentType, response) { 98 | const headers = { 99 | ':status': statusCode, 100 | 'content-type': contentType, 101 | }; 102 | 103 | try { 104 | stream.respond({ headers }); 105 | stream.end(response); 106 | } catch (error) { 107 | tools.logger.error(`Error sending response: ${error.message}`); 108 | } 109 | } 110 | 111 | listen() { 112 | try { 113 | this.#generateCertificates(); 114 | const serverOptions = this.#createServerOptions(); 115 | 116 | this.app = nghttp3.createServer(serverOptions, (stream, headers) => { 117 | this.#handleStream(stream, headers); 118 | }); 119 | 120 | this.app.on('error', (error) => { 121 | tools.logger.error(`HTTP/3 server error: ${error.message}`); 122 | }); 123 | 124 | return new Promise((resolve) => { 125 | this.server = this.app.listen(this.port, () => { 126 | tools.logger.info(`HTTP/3 server listening on port ${this.port}`); 127 | resolve(); 128 | }); 129 | }); 130 | } catch (error) { 131 | tools.logger.error(`Error starting HTTP/3 server: ${error.message}`); 132 | throw error; 133 | } 134 | } 135 | 136 | stop() { 137 | return new Promise((resolve, reject) => { 138 | if (!this.server) { 139 | resolve(); 140 | return; 141 | } 142 | 143 | this.server.close((err) => { 144 | if (err) { 145 | tools.logger.error(`Error stopping HTTP/3 server: ${err.message}`); 146 | reject(err); 147 | } else { 148 | tools.logger.info('HTTP/3 server stopped successfully'); 149 | resolve(); 150 | } 151 | }); 152 | }); 153 | } 154 | } 155 | 156 | module.exports = Http3Server; 157 | -------------------------------------------------------------------------------- /src/adapters/http/HttpLauncher.js: -------------------------------------------------------------------------------- 1 | const BaseLauncher = require('../BaseLauncher'); 2 | const HttpServer = require('./HttpServer'); 3 | const Http2Server = require('./Http2Server'); 4 | // const Http3Server = require('./Http3Server'); 5 | 6 | class HttpLauncher extends BaseLauncher { 7 | servers = new Map(); 8 | constructor(servers) { 9 | super('HttpLauncher'); 10 | servers.forEach(server => { 11 | this.#launchServer(server); 12 | }); 13 | } 14 | 15 | #launchServer(server) { 16 | if (server.type.toUpperCase() === 'QUIC') { 17 | // this.servers.set(`${server.type}:${server.name}`, new Http3Server(server)); 18 | } else if (server.ssl && server.ssl === true) { 19 | this.servers.set(`${server.type}:${server.name}`, new Http2Server(server)); 20 | } else { 21 | this.servers.set(`${server.type}:${server.name}`, new HttpServer(server)); 22 | } 23 | } 24 | 25 | 26 | } 27 | 28 | module.exports = HttpLauncher; 29 | -------------------------------------------------------------------------------- /src/adapters/http/HttpServer.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const BaseServer = require('../BaseServer'); 3 | const url = require('url'); 4 | 5 | class HttpServer extends BaseServer { 6 | status = false; 7 | 8 | constructor(config) { 9 | super(config); 10 | } 11 | 12 | #parseQueryParams(req) { 13 | const baseUrl = `http://${req.headers.host}`; 14 | const parsedUrl = new URL(req.url, baseUrl); 15 | const queryParams = Object.fromEntries(parsedUrl.searchParams.entries()); 16 | return queryParams; 17 | } 18 | 19 | listen() { 20 | try { 21 | 22 | this.app = http.createServer((req, res) => { 23 | 24 | this.#setCORSHeaders(req, res); 25 | 26 | if (req.method === 'OPTIONS') { 27 | res.writeHead(204); 28 | res.end(); 29 | return; 30 | } 31 | 32 | 33 | 34 | const queryParams = this.#parseQueryParams(req); 35 | 36 | let body = ''; 37 | 38 | req.on('data', (chunk) => { 39 | body += chunk.toString(); 40 | }); 41 | 42 | req.on('end', () => { 43 | let inputData = null; 44 | if (req.headers['content-type'] === 'application/json') { 45 | try { 46 | inputData = JSON.parse(body); 47 | } catch (error) { 48 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 49 | res.end('Invalid JSON format'); 50 | return; 51 | } 52 | } 53 | 54 | this.handleIncomingRequest({ type: 'HTTP', data: req, inputData, queryParams }, res).then(command => { 55 | const contentType = command?.dispatcher?.contentType || 'text/plain'; 56 | res.writeHead(command?.statusCode || 400, { 'Content-Type': contentType }); 57 | switch (contentType) { 58 | case 'text/json': 59 | res.end(JSON.stringify(command.response)); 60 | break; 61 | default: 62 | res.end(command.response.toString()); 63 | break; 64 | } 65 | }).catch((error) => { 66 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 67 | res.end(error.toString()); 68 | }); 69 | }); 70 | 71 | req.on('error', (error) => { 72 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 73 | res.end(`Request error: ${error.message}`); 74 | }); 75 | }); 76 | 77 | return new Promise((resolve) => { 78 | this.server = this.app.listen(this.port, () => { 79 | resolve(); 80 | }); 81 | }); 82 | } catch (error) { 83 | this.error(`Error starting HTTP server: ${error.message}`); 84 | throw error; 85 | } 86 | } 87 | 88 | #setCORSHeaders(req, res) { 89 | const origin = req.headers.origin; 90 | res.setHeader('Access-Control-Allow-Origin', origin || '*'); 91 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 92 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 93 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); 94 | } 95 | 96 | stop() { 97 | return new Promise((resolve, reject) => { 98 | this.server.close((err) => { 99 | if (err) { 100 | this.error(`Error stopping HTTP server: ${err.message}`); 101 | reject(err); 102 | } else { 103 | resolve(); 104 | } 105 | }); 106 | }); 107 | } 108 | } 109 | 110 | module.exports = HttpServer; 111 | -------------------------------------------------------------------------------- /src/adapters/rpc/RpcLauncher.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require('../../config/ConfigCenter'); 2 | const BaseLauncher = require('../BaseLauncher'); 3 | const RpcServer = require('./RpcServer'); 4 | 5 | class RpcLauncher extends BaseLauncher { 6 | #config = null; 7 | servers = new Map(); 8 | constructor() { 9 | super('RpcLauncher'); 10 | this.#config = ConfigCenter.getInstance().get('rpc'); 11 | this.servers.set('rpc' , new RpcServer(this.#config)) 12 | } 13 | 14 | } 15 | 16 | module.exports = RpcLauncher; 17 | -------------------------------------------------------------------------------- /src/adapters/rpc/RpcServer.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const BaseServer = require('../BaseServer'); 3 | 4 | class RpcServer extends BaseServer { 5 | constructor(config) { 6 | super(config); 7 | this.server = net.createServer(); 8 | this.handlers = {}; 9 | } 10 | 11 | registerMethod(method, handler) { 12 | this.handlers[method] = handler; 13 | } 14 | 15 | listen() { 16 | this.server.on('connection', (socket) => { 17 | socket.on('data', (data) => { 18 | try { 19 | const request = JSON.parse(data.toString()); 20 | return this.handleIncomingRequest({ type: 'RPC', data: request }); 21 | } catch (err) { 22 | const errorResponse = { 23 | id: null, 24 | result: null, 25 | error: 'Invalid request format', 26 | }; 27 | socket.write(JSON.stringify(errorResponse)); 28 | } 29 | }); 30 | }); 31 | 32 | return new Promise((resolve) => { 33 | this.server.listen(this.port, () => { 34 | resolve(); 35 | }); 36 | }); 37 | } 38 | 39 | stop() { 40 | return new Promise((resolve) => { 41 | this.server.close(() => { 42 | resolve(); 43 | }); 44 | }); 45 | } 46 | } 47 | 48 | module.exports = RpcServer; 49 | -------------------------------------------------------------------------------- /src/application/Application.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require("../config/ConfigCenter"); 2 | const CommandDispatcher = require("./command/CommandDispatcher"); 3 | const Database = require("./database/Database"); 4 | const EventManager = require("./events/EventManager"); 5 | const Events = require("./events/Events"); 6 | const Loader = require("./loader/Loader"); 7 | const PackageManager = require("./loader/Packages"); 8 | const MiddlewareManager = require("./middleware/MiddlewareManager"); 9 | 10 | class Application { 11 | constructor() { 12 | this.loader = new Loader(); 13 | this.packages = new PackageManager(); 14 | this.eventManager = new EventManager(); 15 | this.dispathcer = new CommandDispatcher(); 16 | this.events = new Events(); 17 | this.database = new Database(); 18 | this.middlewareManager = new MiddlewareManager(); 19 | } 20 | run() { 21 | this.database.autoLoadDatabases(); 22 | this.loader.registerLoaders(); 23 | this.packages.registerPackages(); 24 | this.dispathcer.registerCommands(); 25 | this.middlewareManager.registerMiddlewares(); 26 | this.events.registerEvents(); 27 | } 28 | 29 | } 30 | 31 | module.exports = Application; -------------------------------------------------------------------------------- /src/application/command/Command.js: -------------------------------------------------------------------------------- 1 | const { tools } = require("../../utils/ToolManager"); 2 | const CommandParser = require("./CommandParser"); 3 | 4 | class Command { 5 | constructor(request) { 6 | this.#init(request); 7 | } 8 | 9 | #init(request) { 10 | try { 11 | const {data, type} = new CommandParser(request).parse(); 12 | this.data = data; 13 | this.type = type; 14 | } catch (error) { 15 | tools.logger.error(`command failed`); 16 | tools.logger.error(error); 17 | } 18 | } 19 | 20 | pattern() { 21 | this.signature = Command.pattern(this.data); 22 | return this.signature; 23 | } 24 | 25 | setSession(session) { 26 | this.data.session = session; 27 | } 28 | 29 | getSession() { 30 | return this.data.session; 31 | } 32 | 33 | setResponse(response) { 34 | this.response = response; 35 | } 36 | 37 | setDispatcher(dispatcher) { 38 | this.dispatcher = dispatcher; 39 | } 40 | 41 | setError(error) { 42 | this.error = error; 43 | } 44 | 45 | setStatusCode(status) { 46 | this.statusCode = status; 47 | } 48 | 49 | static pattern(data) { 50 | const { type, protocol, method, target } = data; 51 | return `COMMAND:${type}.${protocol}:${method}:${target.toUpperCase()}`; 52 | } 53 | } 54 | 55 | module.exports = Command; 56 | -------------------------------------------------------------------------------- /src/application/command/CommandDispatcher.js: -------------------------------------------------------------------------------- 1 | const { tools } = require('../../utils/ToolManager'); 2 | const EventManager = require('../events/EventManager'); 3 | const MiddlewareManager = require('../middleware/MiddlewareManager'); 4 | const Command = require('./Command'); 5 | const CommandRouter = require('./CommandRouter'); 6 | 7 | class CommandDispatcher { 8 | constructor( 9 | eventManager = EventManager, 10 | commandRouter = new CommandRouter(this) 11 | ) { 12 | this.handlers = new Map(); 13 | this.emitter = eventManager.getInstance().emitter; 14 | this.commandRouter = commandRouter; 15 | } 16 | 17 | /** 18 | * Registers a handler for a given command descriptor. 19 | * @param {Object} commandDescriptor - The descriptor defining the command and its routes. 20 | * @param {Object} handler - The handler object containing methods to handle the command. 21 | */ 22 | registerCommandHandler(commandDescriptor, handler) { 23 | commandDescriptor.routes.forEach(route => { 24 | const pattern = Command.pattern({ ...commandDescriptor, ...route }); 25 | if (this.handlers.has(pattern)) { 26 | throw new Error(`handler for command pattern '${pattern}' is already registered.`); 27 | } 28 | this.handlers.set(pattern, { handler, method: route.handler, middlewares: route.middlewares }); 29 | }); 30 | } 31 | 32 | /** 33 | * Subscribes to a command pattern and listens for incoming commands. 34 | * @param {Object} descriptor - The descriptor for the command pattern. 35 | * @param {Object} [payload={}] - Optional payload data. 36 | */ 37 | subscribeToCommandPattern(descriptor, loaded) { 38 | if (!descriptor) return; 39 | const requestPattern = Command.pattern(descriptor); 40 | this.emitter.subscribe(requestPattern, (command) => { 41 | try { 42 | this.dispatchCommand(requestPattern, loaded, command).then(response => { 43 | command.setResponse(response); 44 | command.setStatusCode(200); 45 | command.setDispatcher(descriptor); 46 | this.emitter.publish(`${command.signature}:RESPONSE`, command); 47 | }); 48 | } catch (error) { 49 | tools.logger.error('publish to command failed'); 50 | tools.logger.error(error); 51 | command.setError(error); 52 | command.setStatusCode(400); 53 | this.emitter.publish(`${command.signature}:RESPONSE`, command); 54 | } 55 | }); 56 | tools.logger.info(`command handler loaded: ${requestPattern}`); 57 | } 58 | 59 | async #runMiddlewares(middlewares, command, payload = {}) { 60 | middlewares = middlewares.reverse(); 61 | 62 | class NextError extends Error { 63 | constructor() { 64 | super('Next called'); 65 | this.name = 'NextError'; 66 | } 67 | } 68 | 69 | const next = async (index) => { 70 | if (index >= middlewares.length) { 71 | return; 72 | } 73 | 74 | const middleware = middlewares[index]; 75 | let callableMiddleware; 76 | 77 | if (typeof middleware === 'object' && typeof middleware.handle === 'function') { 78 | callableMiddleware = middleware.handle.bind(middleware); 79 | } else if (typeof middleware === 'function') { 80 | callableMiddleware = middleware; 81 | } else { 82 | throw new Error(`Middleware at index ${index} is not a valid middleware`); 83 | } 84 | 85 | try { 86 | await callableMiddleware(command, async () => { 87 | throw new NextError(); 88 | }, payload); 89 | } catch (error) { 90 | if (error instanceof NextError) { 91 | await next(index + 1); 92 | } else { 93 | throw { 94 | errorMessage: error.message || `Error while running middleware at index ${index}, error: ${error.toString()}`, 95 | middleware: middleware.options?.middlewareName || `Middleware at index ${index}`, 96 | originalError: error, // Include the original error for debugging 97 | }; 98 | } 99 | } 100 | }; 101 | 102 | // Start the middleware chain 103 | try { 104 | await next(0); 105 | } catch (error) { 106 | // Handle the final error and log it 107 | console.error('Middleware chain failed:', error.errorMessage); 108 | throw error; // Rethrow the error if needed 109 | } 110 | } 111 | #loadMiddlewares(middlewares) { 112 | const loadedMiddlewares = []; 113 | if (typeof middlewares === 'string') { 114 | const middleware = MiddlewareManager.getMiddleware(middlewares); 115 | loadedMiddlewares.push(middleware); 116 | } else if (typeof middlewares === 'function') { 117 | loadedMiddlewares.push(middlewares); 118 | } else if (typeof middlewares === 'object') { 119 | middlewares.forEach(middleware => { 120 | loadedMiddlewares.push(...this.#loadMiddlewares(middleware)); 121 | }); 122 | } 123 | return loadedMiddlewares; 124 | } 125 | 126 | 127 | /** 128 | * Dispatches a command to its appropriate handler. 129 | * @param {string} pattern - The command pattern to dispatch. 130 | * @param {Object} payload - The payload for the command. 131 | * @returns {Promise} - The result of the command execution. 132 | */ 133 | async dispatchCommand(pattern, payload = {}, command) { 134 | const { handler, method, middlewares } = this.handlers.get(pattern) || {}; 135 | if (!handler || typeof handler[method] !== 'function') { 136 | throw new Error(`handler or method '${method}' not found for pattern: '${pattern}'.`); 137 | } 138 | 139 | handler.command = command.data; 140 | let handlerMiddlewares; 141 | let beforeMiddlewares = []; 142 | let afterMiddlewares = []; 143 | if (!middlewares) { 144 | return handler[method](payload); 145 | } else { 146 | handlerMiddlewares = this.#loadMiddlewares(middlewares); 147 | handlerMiddlewares.forEach(middleware => { 148 | if (middleware?.options?.type && middleware?.options?.type === 'after') { 149 | afterMiddlewares.push(middleware); 150 | } else { 151 | beforeMiddlewares.push(middleware); 152 | } 153 | }); 154 | } 155 | 156 | 157 | const handlerProxy = async () => { 158 | let result; 159 | let afterResult 160 | let beforeMiddlewaresStatus = true; 161 | try { 162 | if (beforeMiddlewares) { 163 | await this.#runMiddlewares(beforeMiddlewares, command); 164 | } 165 | } catch (error) { 166 | if (typeof error === 'object') { 167 | tools.logger.error(`Error while running middleware ${error.middleware}`); 168 | tools.logger.error(error.errorMessage); 169 | beforeMiddlewaresStatus = new Error(error.errorMessage) 170 | } else { 171 | tools.logger.error('Error while running middlewares'); 172 | tools.logger.error(error); 173 | beforeMiddlewaresStatus = error 174 | } 175 | } 176 | 177 | if (beforeMiddlewaresStatus === true) { 178 | result = await handler[method](payload); 179 | } else { 180 | result = beforeMiddlewaresStatus.message || 'error while running middlewares'; 181 | } 182 | afterResult = result; 183 | 184 | try { 185 | if (afterMiddlewares) { 186 | let index = 0; 187 | const next = function (newResult = false) { 188 | afterResult = newResult; 189 | index++; 190 | } 191 | while (index < afterMiddlewares.length) { 192 | const middleware = afterMiddlewares[index]; 193 | try { 194 | let oldIndex = index; 195 | await middleware.handle(command, next, afterResult); 196 | if (oldIndex === index) { 197 | index++; 198 | } 199 | } catch (error) { 200 | console.log('Error while running after middleware', error); 201 | } 202 | } 203 | } 204 | } catch (error) { 205 | console.log('error whille running after middlewares'); 206 | } 207 | 208 | if (afterMiddlewares) { 209 | return afterResult; 210 | } else { 211 | return result; 212 | } 213 | }; 214 | 215 | return handlerProxy(); 216 | } 217 | /** 218 | * Automatically registers commands via the CommandRouter. 219 | */ 220 | registerCommands() { 221 | this.commandRouter.registerCommands(); 222 | } 223 | } 224 | 225 | module.exports = CommandDispatcher; 226 | -------------------------------------------------------------------------------- /src/application/command/CommandParser.js: -------------------------------------------------------------------------------- 1 | const { tools } = require("../../utils/ToolManager"); 2 | 3 | class CommandParser { 4 | #request = null; 5 | constructor(request) { 6 | this.#request = request; 7 | } 8 | 9 | parse() { 10 | return this.#parseCommandFromRequest(); 11 | } 12 | 13 | #parseCommandFromRequest() { 14 | let command; 15 | switch (this.#request.type) { 16 | case 'HTTP': 17 | case 'HTTPS': 18 | command = this.#httpRequestToCommand(this.#request.data, this.#request.type, this.#request.inputData, this.#request.queryParams); 19 | break; 20 | case 'RPC': 21 | command = this.#rpcRequestToCommand(this.#request.data); 22 | break; 23 | default: 24 | command = this.#genericRequestToCommand(this.#request.type, this.#request.data); 25 | tools.logger.error(`unsupported request type ${this.#request.type}`); 26 | } 27 | return { 28 | type: this.type, 29 | data: command 30 | }; 31 | } 32 | 33 | #validateRoute(route) { 34 | const regex = /^\/([^?]*)/; const match = route.match(regex); 35 | return match ? `/${match[1]}`.toUpperCase() : null; } 36 | 37 | #httpRequestToCommand(httpRequest, protocol, inputData = false , queryParams = false) { 38 | const { method, url, body, query, httpVersion, headers, statusCode } = httpRequest; 39 | 40 | const meta = { 41 | timestamp: new Date().toISOString(), 42 | // headers: headers || {}, 43 | httpVersion, 44 | contentType: headers?.['content-type'] || null, 45 | }; 46 | 47 | return { 48 | type: 'REQUEST', 49 | protocol: protocol.toUpperCase(), 50 | method: method.toUpperCase(), 51 | target: this.#validateRoute(url), 52 | statusCode: statusCode || 200, 53 | inputData: inputData || body || {}, 54 | queryParams: queryParams || {}, 55 | meta, 56 | }; 57 | } 58 | 59 | #rpcRequestToCommand(rpcRequest) { 60 | const { serviceName, methodName, payload } = rpcRequest; 61 | const target = `${serviceName}/${methodName}`; 62 | 63 | return { 64 | type: 'REQUEST', 65 | protocol: 'RPC', 66 | method: methodName.toUpperCase(), 67 | target, 68 | payload: payload || {} 69 | }; 70 | } 71 | 72 | #genericRequestToCommand(type, data) { 73 | return { 74 | type: 'UNKNOWN', 75 | protocol: type.toUpperCase(), 76 | method: 'UNKNOWN', 77 | target: 'UNKNOWN', 78 | payload: data || {} 79 | }; 80 | } 81 | 82 | } 83 | 84 | module.exports = CommandParser; -------------------------------------------------------------------------------- /src/application/command/CommandRouter.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require("../../config/ConfigCenter"); 2 | const { tools } = require("../../utils/ToolManager"); 3 | const LoaderResolver = require("../loader/LoaderResolver"); 4 | const path = require("path"); 5 | 6 | class CommandRouter { 7 | constructor(dispatcher) { 8 | this.dispatcher = dispatcher; 9 | this.handlersPath = ConfigCenter.getInstance().get("commandsPath") || false; 10 | } 11 | 12 | validateCommandFile(filePath) { 13 | if (path.extname(filePath) !== ".js") return false; 14 | try { 15 | const CommandClass = require(filePath); 16 | return this.validateDescriptor(CommandClass.descriptor); 17 | } catch (error) { 18 | tools.logger.error(`cannot load entities`) 19 | tools.logger.error(error) 20 | return; 21 | } 22 | } 23 | 24 | validateDescriptor(descriptor) { 25 | return ( 26 | descriptor?.commandName && 27 | descriptor?.type && 28 | descriptor?.protocol && 29 | Array.isArray(descriptor?.routes) 30 | ); 31 | } 32 | 33 | loadEntities(loader) { 34 | return LoaderResolver.resolveLoaderEntities(loader) || {}; 35 | } 36 | 37 | registerCommand(filePath) { 38 | try { 39 | if (!this.validateCommandFile(filePath)) { 40 | tools.logger.error(`Invalid command file` , filePath) 41 | return; 42 | } 43 | 44 | const CommandClass = require(filePath); 45 | const loadedEntities = this.loadEntities(CommandClass.descriptor.loader); 46 | const handlerInstance = new CommandClass(loadedEntities); 47 | 48 | handlerInstance.descriptor = CommandClass.descriptor; 49 | 50 | if (handlerInstance.descriptor?.routes) { 51 | this.dispatcher.registerCommandHandler( 52 | handlerInstance.descriptor, 53 | handlerInstance 54 | ); 55 | 56 | const routes = handlerInstance.descriptor.routes; 57 | 58 | routes.forEach((route) => { 59 | const descriptor = { ...handlerInstance.descriptor, ...route }; 60 | const routeEntities = this.loadEntities(route.loader); 61 | this.dispatcher.subscribeToCommandPattern(descriptor, routeEntities); 62 | }); 63 | } else { 64 | tools.logger.warn(`skipping file: ${filePath}. Descriptor or routes are invalid`) 65 | } 66 | } catch (error) { 67 | tools.logger.error(`failed to register command from` , filePath); 68 | tools.logger.error(error); 69 | } 70 | } 71 | 72 | registerCommands() { 73 | try { 74 | const files = LoaderResolver.getFiles(this.handlersPath); 75 | files.forEach((file) => this.registerCommand(file)); 76 | } catch (error) { 77 | tools.logger.error(`error while registering commands`); 78 | tools.logger.error(error); 79 | } 80 | } 81 | } 82 | 83 | module.exports = CommandRouter; 84 | -------------------------------------------------------------------------------- /src/application/database/Database.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require('../../config/ConfigCenter'); 2 | const { tools } = require('../../utils/ToolManager'); 3 | const DatabaseInterface = require('./DatabaseInterface'); 4 | 5 | class Database { 6 | static adapter = null; 7 | #config; 8 | adapters = { 9 | mongodb: require('./adapters/MongoInterface'), 10 | redis: require('./adapters/RedisInterface'), 11 | mysql: require('./adapters/MySqlInterface'), 12 | sqlite: require('./adapters/SqlLiteInterface'), 13 | }; 14 | 15 | constructor() { 16 | this.databases = {}; 17 | this.#config = ConfigCenter.getInstance().get('database') || false; 18 | } 19 | 20 | autoLoadDatabases() { 21 | if ( 22 | typeof this.#config !== 'object' || 23 | this.#config.length <= 0 24 | ) { 25 | tools.logger.warn('database config not found') 26 | return false; 27 | } 28 | if (Database.adapter !== null) { 29 | tools.logger.warn('databases already loaded'); 30 | return false; 31 | } 32 | try { 33 | this.#loadDatabases(); 34 | Database.adapter = new DatabaseInterface(this); 35 | } catch (error) { 36 | tools.logger.error(`error while loading databases`); 37 | tools.logger.error(error); 38 | } 39 | 40 | } 41 | 42 | #loadDatabases() { 43 | for (const [key, dbConfig] of Object.entries(this.#config)) { 44 | const { type, ...options } = dbConfig; 45 | const Adapter = this.adapters[type.toLowerCase()]; 46 | if (!Adapter) { 47 | tools.logger.error(`Unknown database type: ${type}`); 48 | } 49 | try { 50 | this.databases[key] = new Adapter(options); 51 | if (typeof this.databases[key].initialQuery === 'function') { 52 | this.databases[key].initialQuery().then(() => { 53 | tools.logger.info(`initial query ran for database ${key} type: ${type}`); 54 | }); 55 | } 56 | tools.logger.info(`database ${key} registerd with type: ${type}`); 57 | } catch (error) { 58 | tools.logger.error(`cannot load database ${key} type: ${type}`); 59 | tools.logger.error(error); 60 | } 61 | } 62 | } 63 | 64 | getDatabase(key) { 65 | return this.databases[key]; 66 | } 67 | } 68 | 69 | module.exports = Database; -------------------------------------------------------------------------------- /src/application/database/DatabaseAdapter.js: -------------------------------------------------------------------------------- 1 | class DatabaseAdapter { 2 | constructor(config) { 3 | this.config = config; 4 | this.connection = null; 5 | } 6 | 7 | async connect() { 8 | throw new Error('connect method must be implemented.'); 9 | } 10 | 11 | async disconnect() { 12 | throw new Error('disconnect method must be implemented.'); 13 | } 14 | 15 | async query(sql, params = []) { 16 | throw new Error('query method must be implemented.'); 17 | } 18 | } 19 | 20 | module.exports = DatabaseAdapter; -------------------------------------------------------------------------------- /src/application/database/DatabaseInterface.js: -------------------------------------------------------------------------------- 1 | const { tools } = require("../../utils/ToolManager"); 2 | 3 | class DatabaseInterface { 4 | constructor(dbManager) { 5 | this.dbManager = dbManager; 6 | this.connections = {}; 7 | } 8 | 9 | async getConnection(key) { 10 | if (this.connections[key]) { 11 | tools.logger.info(`using cached connection for ${key}`); 12 | return this.connections[key]; 13 | } 14 | const db = this.dbManager.getDatabase(key); 15 | await db.connect(); 16 | this.connections[key] = db; 17 | tools.logger.info(`created and cached connection for ${key}`); 18 | return db; 19 | } 20 | 21 | async closeConnection(key) { 22 | if (this.connections[key]) { 23 | await this.connections[key].disconnect(); 24 | delete this.connections[key]; 25 | tools.logger.info(`closed connection for ${key}`); 26 | } else { 27 | tools.logger.info(`no active connection found for ${key}`); 28 | } 29 | } 30 | 31 | async closeAllConnections() { 32 | for (const key of Object.keys(this.connections)) { 33 | await this.closeConnection(key); 34 | } 35 | tools.logger.info('all connections closed'); 36 | } 37 | 38 | async query(key, ...args) { 39 | const db = await this.getDatabase(key); 40 | return await db.query(...args); 41 | } 42 | 43 | isConnectionActive(key) { 44 | return !!this.connections[key]; 45 | } 46 | 47 | getActiveConnections() { 48 | return Object.keys(this.connections); 49 | } 50 | } 51 | 52 | module.exports = DatabaseInterface; -------------------------------------------------------------------------------- /src/application/database/adapters/MongoInterface.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require('mongodb'); 2 | const DatabaseAdapter = require('../DatabaseAdapter'); 3 | const { tools } = require('../../../utils/ToolManager'); 4 | 5 | class MongoInterface extends DatabaseAdapter { 6 | async connect() { 7 | try { 8 | this.connection = await MongoClient.connect(this.config.connectionString, { 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | }); 12 | tools.logger.info('Connected to MongoDB'); 13 | } catch (err) { 14 | tools.logger.error('Error connecting to MongoDB:', err); 15 | throw err; 16 | } 17 | } 18 | 19 | async disconnect() { 20 | if (this.connection) { 21 | try { 22 | await this.connection.close(); 23 | tools.logger.info('Disconnected from MongoDB'); 24 | } catch (err) { 25 | tools.logger.error('Error disconnecting from MongoDB:', err); 26 | throw err; 27 | } 28 | } 29 | } 30 | 31 | async query(collection, operation, ...args) { 32 | try { 33 | const db = this.connection.db(this.config.databaseName); 34 | const coll = db.collection(collection); 35 | 36 | switch (operation) { 37 | case 'insert': 38 | return await coll.insertOne(...args); 39 | case 'find': 40 | return await coll.find(...args).toArray(); 41 | case 'update': 42 | return await coll.updateOne(...args); 43 | case 'delete': 44 | return await coll.deleteOne(...args); 45 | default: 46 | throw new Error(`Unsupported operation: ${operation}`); 47 | } 48 | } catch (err) { 49 | tools.logger.error('Error executing MongoDB query:', err); 50 | throw err; 51 | } 52 | } 53 | } 54 | 55 | module.exports = MongoInterface; -------------------------------------------------------------------------------- /src/application/database/adapters/MySqlInterface.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2/promise'); 2 | const DatabaseAdapter = require('../DatabaseAdapter'); 3 | const { tools } = require('../../../utils/ToolManager'); 4 | 5 | class MySqlInterface extends DatabaseAdapter { 6 | async connect() { 7 | try { 8 | this.connection = await mysql.createConnection({ 9 | host: this.config.host, 10 | user: this.config.user, 11 | password: this.config.password, 12 | database: this.config.database, 13 | }); 14 | tools.logger.info('Connected to MySQL database'); 15 | } catch (err) { 16 | tools.logger.error('Error connecting to MySQL:'); 17 | tools.logger.error(err); 18 | return; 19 | } 20 | } 21 | 22 | async disconnect() { 23 | if (this.connection) { 24 | try { 25 | await this.connection.end(); 26 | tools.logger.info('Disconnected from MySQL database'); 27 | } catch (err) { 28 | tools.logger.error('Error disconnecting from MySQL:', err); 29 | tools.logger.error(err); 30 | return; 31 | } 32 | } 33 | } 34 | 35 | async query(sql, params = []) { 36 | try { 37 | const [rows] = await this.connection.execute(sql, params); 38 | return rows; 39 | } catch (err) { 40 | tools.logger.error('Error executing query:', err); 41 | return; 42 | } 43 | } 44 | } 45 | 46 | module.exports = MySqlInterface; -------------------------------------------------------------------------------- /src/application/database/adapters/RedisInterface.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const { promisify } = require('util'); 3 | const DatabaseAdapter = require('../DatabaseAdapter'); 4 | const { tools } = require('../../../utils/ToolManager'); 5 | 6 | class RedisInterface extends DatabaseAdapter { 7 | async connect() { 8 | try { 9 | this.connection = redis.createClient({ 10 | host: this.config.host, 11 | port: this.config.port, 12 | }); 13 | 14 | this.connection.getAsync = promisify(this.connection.get).bind(this.connection); 15 | this.connection.setAsync = promisify(this.connection.set).bind(this.connection); 16 | this.connection.delAsync = promisify(this.connection.del).bind(this.connection); 17 | 18 | this.connection.on('connect', () => { 19 | tools.logger.info('Connected to Redis'); 20 | }); 21 | 22 | this.connection.on('error', (err) => { 23 | tools.logger.error('Redis error:', err); 24 | }); 25 | } catch (err) { 26 | tools.logger.error('Error connecting to Redis:', err); 27 | throw err; 28 | } 29 | } 30 | 31 | async disconnect() { 32 | if (this.connection) { 33 | try { 34 | await this.connection.quit(); 35 | tools.logger.info('Disconnected from Redis'); 36 | } catch (err) { 37 | tools.logger.error('Error disconnecting from Redis:', err); 38 | throw err; 39 | } 40 | } 41 | } 42 | 43 | async query(command, ...args) { 44 | try { 45 | const result = await this.connection[`${command}Async`](...args); 46 | return result; 47 | } catch (err) { 48 | tools.logger.error('Error executing Redis command:', err); 49 | throw err; 50 | } 51 | } 52 | } 53 | 54 | module.exports = RedisInterface; -------------------------------------------------------------------------------- /src/application/database/adapters/SqlLiteInterface.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3').verbose(); 2 | const { tools } = require('../../../utils/ToolManager'); 3 | const DatabaseAdapter = require('../DatabaseAdapter'); 4 | 5 | class SqlLiteInterface extends DatabaseAdapter { 6 | 7 | async connect() { 8 | return new Promise((resolve, reject) => { 9 | this.connection = new sqlite3.Database(this.config.filename, (err) => { 10 | if (err) { 11 | reject(err); 12 | } else { 13 | tools.logger.info(`connected to ${this.config.filename} database`); 14 | resolve(); 15 | } 16 | }); 17 | }); 18 | } 19 | 20 | async initialQuery() { 21 | await this.connect() 22 | if (typeof this.config.initialQuery === 'string') { 23 | this.query(this.config.initialQuery); 24 | } else if (typeof this.config.initialQuery === 'object') { 25 | this.config.initialQuery.forEach(query => { 26 | this.query(query); 27 | }); 28 | } 29 | } 30 | 31 | async disconnect() { 32 | return new Promise((resolve, reject) => { 33 | this.connection.close((err) => { 34 | if (err) { 35 | reject(err); 36 | } else { 37 | tools.logger.info(`disconnected from ${this.config.filename} database`); 38 | resolve(); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | async query(sql, params = []) { 45 | return new Promise((resolve, reject) => { 46 | this.connection.all(sql, params, (err, rows) => { 47 | if (err) { 48 | reject(err); 49 | } else { 50 | resolve(rows); 51 | } 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | module.exports = SqlLiteInterface; -------------------------------------------------------------------------------- /src/application/events/EventManager.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require("../../config/ConfigCenter"); 2 | const { tools } = require("../../utils/ToolManager"); 3 | 4 | class EventManager { 5 | static #instance = null; 6 | 7 | constructor() { 8 | if (EventManager.#instance) { 9 | return EventManager.#instance; 10 | } 11 | this.config = ConfigCenter.getInstance().get('event'); 12 | this.type = this.config.emitter || "eventemitter2"; 13 | this.emitter = this.#initializeEmitter(this.type); 14 | tools.logger.info(`${this.type} event manager created`); 15 | EventManager.#instance = this; 16 | } 17 | 18 | #initializeEmitter(type) { 19 | switch (type) { 20 | case "eventemitter2": 21 | const EventEmitter2Interface = require("./interfaces/EventEmitter2"); 22 | return new EventEmitter2Interface(); 23 | case "kafka": 24 | const KafkaInterface = require("./interfaces/Kafka"); 25 | return new KafkaInterface(); 26 | case "mq": 27 | const RabbitMQInterface = require("./interfaces/RabbitMQ"); 28 | return new RabbitMQInterface(); 29 | default: 30 | throw new Error(`[EventManager] Unsupported event system type: ${type}`); 31 | } 32 | } 33 | 34 | static getInstance() { 35 | if (!EventManager.#instance) { 36 | EventManager.#instance = new EventManager(); 37 | } 38 | return EventManager.#instance; 39 | } 40 | } 41 | 42 | module.exports = EventManager; 43 | -------------------------------------------------------------------------------- /src/application/events/Events.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require("../../config/ConfigCenter"); 2 | const { tools } = require("../../utils/ToolManager"); 3 | const LoaderResolver = require("../loader/LoaderResolver"); 4 | const EventManager = require("./EventManager"); 5 | 6 | class Events { 7 | static publish = (eventName, data) => { 8 | tools.logger.warn(`still event publisher not initilized.`, eventName); 9 | }; 10 | 11 | constructor() { 12 | this.eventsPath = ConfigCenter.getInstance().get("eventsPath") || false; 13 | this.emitter = EventManager.getInstance().emitter; 14 | } 15 | 16 | registerEvents() { 17 | try { 18 | const files = LoaderResolver.getFiles(this.eventsPath); 19 | files.forEach((file) => this.#registerEvent(file)); 20 | Events.publish = (eventName, data, response = false) => { 21 | if (response && typeof response === 'function') { 22 | EventManager.getInstance().emitter.subscribe(`EVENT:${eventName}:RESPONSE`, (incoming) => { 23 | response(incoming); 24 | }); 25 | EventManager.getInstance().emitter.publish(`EVENT:${eventName}`, { ...data, __response: true }); 26 | } else { 27 | EventManager.getInstance().emitter.publish(`EVENT:${eventName}`, data); 28 | } 29 | }; 30 | } catch (error) { 31 | tools.logger.error(`error while registering events`); 32 | tools.logger.error(error); 33 | } 34 | } 35 | 36 | #registerEvent(filePath) { 37 | try { 38 | const eventClass = LoaderResolver.loadJsFile(filePath); 39 | if ( 40 | !eventClass && 41 | !this.#validateEventOptions(eventClass?.eventOptions) 42 | ) { 43 | tools.logger.warn(`invalid event file`, filePath); 44 | return; 45 | } 46 | const eventInstance = LoaderResolver.createInstanceAndLoad( 47 | eventClass, 48 | eventClass?.eventOptions?.loader 49 | ); 50 | if (!eventInstance || typeof eventInstance.handle !== "function") { 51 | tools.logger.warn( 52 | `failed to create event instance from or handle function not found in event`, 53 | filePath 54 | ); 55 | return; 56 | } 57 | const eventName = eventClass.eventOptions.eventName; 58 | const eventKey = `EVENT:${eventName}`; 59 | this.emitter.subscribe(eventKey, (data) => { 60 | try { 61 | if (data.__response && data.__response === true) { 62 | const response = eventInstance.handle(data); 63 | EventManager.getInstance().emitter.publish(`EVENT:${eventName}:RESPONSE`, response); 64 | } else { 65 | return eventInstance.handle(data); 66 | } 67 | tools.logger.info(`+ event published: ${`EVENT:${eventName}`}`); 68 | } catch (error) { 69 | tools.logger.error(`- publish to event ${eventName} failed`); 70 | tools.logger.error(error); 71 | } 72 | }); 73 | tools.logger.info(`event loaded for pattern: ${eventKey}`); 74 | } catch (error) { 75 | tools.logger.error(`failed to register event from`, filePath); 76 | tools.logger.error(error); 77 | } 78 | } 79 | 80 | #validateEventOptions(options) { 81 | return options?.eventName && options?.type; 82 | } 83 | } 84 | 85 | module.exports = Events; 86 | -------------------------------------------------------------------------------- /src/application/events/interfaces/EventEmitter2.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter2 } = require("eventemitter2"); 2 | 3 | class EventEmitter2Interface { 4 | constructor() { 5 | if (!EventEmitter2Interface.instance) { 6 | this.emitter = new EventEmitter2({ 7 | wildcard: true, 8 | maxListeners: 20, 9 | }); 10 | EventEmitter2Interface.instance = this; 11 | } 12 | return EventEmitter2Interface.instance; 13 | } 14 | 15 | publish(event, message) { 16 | this.emitter.emit(event, message); 17 | } 18 | 19 | subscribe(event, callback) { 20 | this.emitter.on(event, callback); 21 | } 22 | } 23 | 24 | module.exports = EventEmitter2Interface; 25 | -------------------------------------------------------------------------------- /src/application/events/interfaces/Kafka.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require("kafkajs"); 2 | 3 | class KafkaInterface { 4 | constructor() { 5 | const kafka = new Kafka({ 6 | clientId: "my-app", 7 | brokers: ["localhost:9092"], 8 | }); 9 | this.producer = kafka.producer(); 10 | this.consumer = kafka.consumer({ groupId: "command-group" }); 11 | } 12 | 13 | async publish(topic, message) { 14 | await this.producer.send({ 15 | topic, 16 | messages: [{ value: JSON.stringify(message) }], 17 | }); 18 | } 19 | 20 | async subscribe(topic, callback) { 21 | await this.consumer.subscribe({ topic }); 22 | await this.consumer.run({ 23 | eachMessage: async ({ message }) => { 24 | callback(JSON.parse(message.value.toString())); 25 | }, 26 | }); 27 | } 28 | } 29 | 30 | module.exports = KafkaInterface; 31 | -------------------------------------------------------------------------------- /src/application/events/interfaces/RabbitMQ.js: -------------------------------------------------------------------------------- 1 | const amqp = require("amqplib"); 2 | 3 | class RabbitMQInterface { 4 | constructor() { 5 | this.connection = null; 6 | this.channel = null; 7 | } 8 | 9 | async connect() { 10 | if (!this.connection) { 11 | this.connection = await amqp.connect("amqp://localhost"); 12 | this.channel = await this.connection.createChannel(); 13 | } 14 | } 15 | 16 | async publish(queue, message) { 17 | await this.connect(); 18 | await this.channel.assertQueue(queue, { durable: true }); 19 | this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); 20 | } 21 | 22 | async subscribe(queue, callback) { 23 | await this.connect(); 24 | await this.channel.assertQueue(queue, { durable: true }); 25 | await this.channel.consume(queue, (msg) => { 26 | if (msg) { 27 | callback(JSON.parse(msg.content.toString())); 28 | this.channel.ack(msg); 29 | } 30 | }); 31 | } 32 | 33 | async close() { 34 | if (this.connection) { 35 | await this.connection.close(); 36 | this.connection = null; 37 | this.channel = null; 38 | } 39 | } 40 | } 41 | 42 | module.exports = RabbitMQInterface; 43 | -------------------------------------------------------------------------------- /src/application/extends/BaseEntity.js: -------------------------------------------------------------------------------- 1 | 2 | class BaseEntity { 3 | constructor(schema, params) { 4 | if (!params) { 5 | return; 6 | } 7 | this.errors = this.#validateRequired(schema.require, params); 8 | } 9 | 10 | #validateRequired(requiredSchema, params) { 11 | const invalids = new Map(); 12 | if (requiredSchema) { 13 | const reqiredFields = Object.keys(requiredSchema); 14 | const reqiredFilters = Object.values(requiredSchema); 15 | for (let index = 0; index < reqiredFields.length; index++) { 16 | const requiredKey = reqiredFields[index]; 17 | const requiredFilter = reqiredFilters[index]; 18 | if (!params[requiredKey] || params[requiredKey] === '') { 19 | invalids.set(requiredKey, 'undefined'); 20 | continue; 21 | } 22 | if (requiredFilter && typeof params[requiredKey] !== requiredFilter) { 23 | // change this later ... , could be a regex 24 | invalids.set(requiredKey, 'invalid'); 25 | } 26 | 27 | this[requiredKey] = params[requiredKey]; 28 | } 29 | } 30 | return invalids; 31 | 32 | } 33 | 34 | validate() { 35 | if (this.errors.size > 0) { 36 | let errorStack = ''; 37 | this.errors.forEach((error , key) => { 38 | errorStack = errorStack + `${key}: ${error}, `; 39 | }) 40 | throw new Error(errorStack.toString()) 41 | } 42 | } 43 | 44 | } 45 | 46 | module.exports = BaseEntity; -------------------------------------------------------------------------------- /src/application/loader/Loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const ConfigCenter = require('../../config/ConfigCenter'); 4 | const { tools } = require('../../utils/ToolManager'); 5 | 6 | class Loader { 7 | static pool = new Map(); 8 | static get(key) { 9 | const entity = Loader.pool.get(key); 10 | if (entity && typeof entity === 'object') { 11 | return entity; 12 | } else { 13 | tools.logger.error(`${key} entity not found`) 14 | return; 15 | } 16 | } 17 | 18 | static load(entityPath, namespace = '*') { 19 | if (typeof namespace !== 'string') { 20 | tools.logger.warn(`namespace of entity is broken`, entityPath) 21 | return; 22 | } 23 | 24 | if (path.extname(entityPath) !== '.js') { 25 | tools.logger.warn(`entity found but broken, ext should be .js`, entityPath) 26 | return; 27 | } 28 | 29 | let entityInstance; 30 | try { 31 | const Entity = require(entityPath); 32 | entityInstance = new Entity(); 33 | } catch (error) { 34 | tools.logger.error(`file found but without entity`) 35 | tools.logger.error(error) 36 | return; 37 | } 38 | 39 | const entityName = `${namespace}.${entityInstance.key}`; 40 | 41 | if (!(entityInstance.key && typeof entityInstance.key === 'string')) { 42 | tools.logger.warn(`entity found but broken, define a key`, entityPath) 43 | return; 44 | } 45 | if (Loader.pool.get(entityName)) { 46 | tools.logger.warn(`entity found but loaded before`, entityName) 47 | return; 48 | } 49 | 50 | Loader.pool.set(entityName, entityInstance); 51 | tools.logger.info(`entity loaded: ${entityName}`) 52 | return entityInstance; 53 | 54 | } 55 | 56 | constructor() { 57 | this.domainPath = path.join(__dirname, '../../../domain'); 58 | this.loaders = [ 59 | ConfigCenter.getInstance().get('servicesPath'), 60 | ]; 61 | } 62 | 63 | autoLoad(loader) { 64 | if (typeof loader === 'object') { 65 | loader.forEach(object => { 66 | this.#registerLoaderObjectToPool(object); 67 | }); 68 | } else { 69 | tools.logger.error(`error while auto load, make sure loader is object`) 70 | } 71 | } 72 | 73 | registerLoaders() { 74 | this.loaders.forEach(loader => { 75 | this.autoLoad(loader); 76 | }); 77 | } 78 | 79 | #registerLoaderObjectToPool(object) { 80 | 81 | if (!object.path) { 82 | tools.logger.warn(`entity found but broken, define a path`) 83 | return; 84 | } 85 | if (!object.namespace) { 86 | tools.logger.warn(`entity found but broken, define a namespace`) 87 | return; 88 | } 89 | 90 | let entities; 91 | try { 92 | entities = fs.readdirSync(object.path); 93 | } catch (error) { 94 | tools.logger.error(`cannot load entities` , object.path) 95 | tools.logger.error(error) 96 | return; 97 | } 98 | entities.forEach(entity => { 99 | const entityPath = path.join(object.path, entity); 100 | Loader.load(entityPath, object.namespace) 101 | }); 102 | } 103 | } 104 | 105 | module.exports = Loader; -------------------------------------------------------------------------------- /src/application/loader/LoaderResolver.js: -------------------------------------------------------------------------------- 1 | const Loader = require("./Loader"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const { tools } = require("../../utils/ToolManager"); 5 | 6 | class LoaderResolver { 7 | 8 | static getFiles(filePath) { 9 | if (typeof filePath === "string") { 10 | return fs 11 | .readdirSync(filePath) 12 | .map((file) => path.join(filePath, file)); 13 | } 14 | if (Array.isArray(filePath)) { 15 | return filePath.flatMap((dir) => 16 | fs.readdirSync(dir).map((file) => path.join(dir, file)) 17 | ); 18 | } 19 | throw new Error(`no valid "${filePath}" found!`); 20 | } 21 | 22 | static resolveLoaderEntities(loaderConfig) { 23 | if (!loaderConfig) return; 24 | if (typeof loaderConfig === 'object') { 25 | return LoaderResolver.resolveMultipleEntities(loaderConfig); 26 | } else if (typeof loaderConfig === 'string') { 27 | return LoaderResolver.resolveSingleEntitiy(loaderConfig); 28 | } 29 | return; 30 | } 31 | 32 | static resolveMultipleEntities(entityKeys) { 33 | const loadedEntities = new Map(); 34 | entityKeys.forEach(entityPath => { 35 | const entity = Loader.get(entityPath); 36 | if (entity?.key) { 37 | loadedEntities.set(entity.key, entity); 38 | } 39 | }); 40 | return loadedEntities; 41 | } 42 | 43 | static resolveSingleEntitiy(entityPath) { 44 | return Loader.get(entityPath); 45 | } 46 | 47 | static createInstanceAndLoad(classObject , loader) { 48 | let loadedEntities; 49 | try { 50 | if ( 51 | typeof loader === 'string' || 52 | typeof loader === 'object' 53 | ) { 54 | loadedEntities = LoaderResolver.resolveLoaderEntities(loader); 55 | } 56 | return new classObject(loadedEntities); 57 | } catch (error) { 58 | tools.logger.error(`error while loading: ${classObject?.constructor?.name || classObject?.name || 'Uknown'}.`); 59 | tools.logger.error(error); 60 | return; 61 | } 62 | } 63 | 64 | static loadJsFile(filePath) { 65 | if (path.extname(filePath) !== ".js") return false; 66 | try { 67 | return require(filePath); 68 | } catch (error) { 69 | tools.logger.error(`cannot load file ${filePath}`) 70 | tools.logger.error(error) 71 | return false; 72 | } 73 | } 74 | } 75 | 76 | module.exports = LoaderResolver; -------------------------------------------------------------------------------- /src/application/loader/Packages.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const ConfigCenter = require("../../config/ConfigCenter"); 3 | const { tools } = require("../../utils/ToolManager"); 4 | 5 | class PackageValidator { 6 | static validatePackage(packageInput) { 7 | if (typeof packageInput === "string") { 8 | try { 9 | const packageInstance = require(packageInput); 10 | packageInstace; 11 | } catch (error) { 12 | tools.logger.error(`cannot load package: ${packageInput}`, error); 13 | return null; 14 | } 15 | } else if (typeof packageInput === "object" && packageInput !== null) { 16 | return packageInput; 17 | } else { 18 | tools.logger.error(`invalid package input type: ${typeof packageInput}`); 19 | return null; 20 | } 21 | } 22 | 23 | static validateFilePath(filePath) { 24 | if (path.extname(filePath) !== ".js") { 25 | tools.logger.error(`invalid file extension for path: ${filePath}`); 26 | return null; 27 | } 28 | try { 29 | return require(filePath); 30 | } catch (error) { 31 | tools.logger.error(`cannot load package from file: ${filePath}`, error); 32 | return null; 33 | } 34 | } 35 | } 36 | 37 | class Package { 38 | constructor(packageName, instance) { 39 | this.packageName = packageName; 40 | this.instance = instance; 41 | } 42 | } 43 | 44 | class PackageManager { 45 | static #packages = new Map(); 46 | constructor() { 47 | this.packagesList = ConfigCenter.getInstance().get("packages") || []; 48 | } 49 | 50 | static addPackage(packageName, instance) { 51 | if (PackageManager.#packages.has(packageName)) { 52 | tools.logger.warn(`package ${packageName} already exists`); 53 | return; 54 | } 55 | PackageManager.#packages.set(packageName, new Package(packageName, instance)); 56 | tools.logger.info(`package loaded: ${packageName}`); 57 | } 58 | 59 | static getPackage(packageName, options = {}) { 60 | const packageEntry = PackageManager.#packages.get(packageName); 61 | if (!packageEntry) { 62 | tools.logger.warn(`package ${packageName} not found`); 63 | return null; 64 | } 65 | 66 | if (options.createInstance && typeof packageEntry.instance === "function") { 67 | return new packageEntry.instance(); 68 | } 69 | return packageEntry.instance; 70 | } 71 | 72 | #registerPackage(packageInput, packageName) { 73 | try { 74 | let name; 75 | let customName; 76 | let packageInstance; 77 | if (typeof packageInput === "object" && packageInput.name && packageInput.path) { 78 | customName = packageInput.name; 79 | packageInstance = PackageValidator.validateFilePath(packageInput.path); 80 | } else if (typeof packageInput === "string" && path.isAbsolute(packageInput)) { 81 | packageInstance = PackageValidator.validateFilePath(packageInput); 82 | } else { 83 | packageInstance = PackageValidator.validatePackage(packageInput); 84 | } 85 | if (!packageInstance) { 86 | tools.logger.error(`invalid package input`, packageInput); 87 | return; 88 | } 89 | 90 | name = customName || packageName || packageInput || packageInstance.name || packageInstance.constructor.name ; 91 | 92 | PackageManager.addPackage(name, packageInstance); 93 | } catch (error) { 94 | tools.logger.error(`failed to register package`, error); 95 | tools.logger.error(error); 96 | } 97 | } 98 | 99 | registerPackages() { 100 | try { 101 | if (!this.packagesList || this.packagesList.length <= 0) { 102 | tools.logger.info("No packages to register."); 103 | return; 104 | } 105 | 106 | this.packagesList.forEach((packageInput) => { 107 | if (typeof packageInput === "object" && packageInput.name && packageInput.input) { 108 | this.#registerPackage(packageInput.input, packageInput.name); 109 | } else { 110 | this.#registerPackage(packageInput); 111 | } 112 | }); 113 | } catch (error) { 114 | tools.logger.error(`Error while registering packages`, error); 115 | } 116 | } 117 | } 118 | 119 | module.exports = PackageManager; -------------------------------------------------------------------------------- /src/application/middleware/MiddlewareManager.js: -------------------------------------------------------------------------------- 1 | const ConfigCenter = require("../../config/ConfigCenter"); 2 | const { tools } = require("../../utils/ToolManager"); 3 | const LoaderResolver = require("../loader/LoaderResolver"); 4 | const path = require("path"); 5 | 6 | class MiddlewareManager { 7 | static middlewares = new Map(); 8 | constructor() { 9 | this.middlewaresPath = ConfigCenter.getInstance().get("middlewaresPath") || false; 10 | } 11 | 12 | static getMiddleware(middlewareName) { 13 | return MiddlewareManager.middlewares.get(middlewareName); 14 | } 15 | 16 | #validateMiddlewareFile(filePath) { 17 | if (path.extname(filePath) !== ".js") return false; 18 | try { 19 | const MiddlewareClass = require(filePath); 20 | return this.#validateMiddleware(MiddlewareClass.options); 21 | } catch (error) { 22 | tools.logger.error(`cannot load middleware`) 23 | tools.logger.error(error) 24 | return; 25 | } 26 | } 27 | 28 | #validateMiddleware(options) { 29 | return ( 30 | options?.middlewareName 31 | ); 32 | } 33 | 34 | #loadEntities(loader) { 35 | return LoaderResolver.resolveLoaderEntities(loader) || {}; 36 | } 37 | 38 | registerMiddleware(filePath) { 39 | try { 40 | if (!this.#validateMiddlewareFile(filePath)) { 41 | tools.logger.error(`Invalid middleware file` , filePath) 42 | return; 43 | } 44 | 45 | const MiddlewareClass = require(filePath); 46 | const loadedEntities = this.#loadEntities(MiddlewareClass?.options?.loader); 47 | const handlerInstance = new MiddlewareClass(loadedEntities); 48 | if (typeof handlerInstance.handle !== 'function') { 49 | tools.logger.error(`failed to find middleware handle function from` , filePath); 50 | return 51 | } 52 | handlerInstance.options = MiddlewareClass.options; 53 | MiddlewareManager.middlewares.set(MiddlewareClass.options.middlewareName, handlerInstance); 54 | tools.logger.info(`middleware loaded: ${MiddlewareClass.options.middlewareName}`); 55 | 56 | } catch (error) { 57 | tools.logger.error(`failed to register middleware from` , filePath); 58 | tools.logger.error(error); 59 | } 60 | } 61 | 62 | registerMiddlewares() { 63 | try { 64 | if (!this.middlewaresPath || this.middlewaresPath.length <= 0) { 65 | return; 66 | } 67 | const files = LoaderResolver.getFiles(this.middlewaresPath); 68 | files.forEach((file) => this.registerMiddleware(file)); 69 | } catch (error) { 70 | tools.logger.error(`error while registering middlewares`); 71 | tools.logger.error(error); 72 | } 73 | } 74 | } 75 | 76 | module.exports = MiddlewareManager; 77 | -------------------------------------------------------------------------------- /src/config/ConfigCenter.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { tools } = require('../utils/ToolManager'); 3 | require('dotenv').config(); 4 | 5 | class ConfigCenter { 6 | static #instance; 7 | #config; 8 | #initialized = false; 9 | 10 | constructor(environmentPath) { 11 | this.environmentPath = environmentPath; 12 | if (ConfigCenter.#instance) { 13 | throw new Error('ConfigCenter is a singleton. Use getInstance() to access it.'); 14 | } 15 | } 16 | 17 | /** 18 | * Initializes the ConfigCenter. 19 | * Loads and validates configuration based on the current environment. 20 | */ 21 | init() { 22 | if (this.#initialized) { 23 | tools.logger.error('ConfigCenter has already been initialized'); 24 | return; 25 | } 26 | const environment = process.env.NODE_ENV || 'development'; 27 | const baseConfig = this.#loadConfigFile('default'); 28 | const envConfig = this.#loadConfigFile(environment); 29 | 30 | const finalConfig = { 31 | ...baseConfig, 32 | ...envConfig, 33 | credentials: { 34 | keyPath: path.join(process.cwd(), 'credentials', 'server-key.pem'), 35 | certPath: path.join(process.cwd(), 'credentials', 'server-cert.crt'), 36 | } 37 | }; 38 | 39 | tools.logger.info(`config loaded ${this.environmentPath ? 'custom' : 'default'}`, environment); 40 | 41 | this.#config = Object.freeze(finalConfig); 42 | this.#initialized = true; 43 | return this.#config; 44 | } 45 | 46 | /** 47 | * Lazy initialization: Ensures init is called if config is accessed without explicit init(). 48 | * @returns {Object} The configuration object. 49 | */ 50 | #ensureInitialized() { 51 | if (!this.#initialized) { 52 | this.init(); 53 | } 54 | } 55 | 56 | /** 57 | * Loads a configuration file by name. 58 | * @param {string} name - The name of the config file (without extension). 59 | * @returns {Object} The configuration object. 60 | */ 61 | #loadConfigFile(name) { 62 | try { 63 | if (this.environmentPath && typeof this.environmentPath === 'string') { 64 | try { 65 | tools.logger.info('found custom environment config path', this.environmentPath); 66 | return require(path.join(this.environmentPath, `${name}.js`)); 67 | } catch (error) { 68 | tools.logger.error('cannot find custom environment config path', this.environmentPath); 69 | return require(`./environments/${name}.js`); 70 | } 71 | } else { 72 | return require(`./environments/${name}.js`); 73 | } 74 | } catch (error) { 75 | tools.logger.warn(`Configuration file ${name}.js not found. Returning empty object.`); 76 | return {}; 77 | } 78 | } 79 | 80 | /** 81 | * Retrieves the singleton instance of ConfigCenter. 82 | * @returns {ConfigCenter} The singleton instance. 83 | */ 84 | static getInstance(environmentPath) { 85 | if (!ConfigCenter.#instance) { 86 | ConfigCenter.#instance = new ConfigCenter(environmentPath); 87 | } 88 | return ConfigCenter.#instance; 89 | } 90 | 91 | /** 92 | * Retrieves the full configuration or a specific key. 93 | * @param {string} [key] - The key to retrieve. 94 | * @returns {*} The configuration value or full configuration object. 95 | */ 96 | get(key) { 97 | this.#ensureInitialized(); 98 | return key ? this.#config[key] : this.#config; 99 | } 100 | 101 | /** 102 | * Refreshes the configuration by reloading and validating it. 103 | */ 104 | refresh() { 105 | this.#initialized = false; 106 | this.init(); 107 | } 108 | } 109 | 110 | module.exports = ConfigCenter; -------------------------------------------------------------------------------- /src/config/environments/default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "event": { 5 | "emitter": "eventemitter2" 6 | }, 7 | "packages": [ 8 | 'http' 9 | ], 10 | "commandsPath": [ 11 | path.join(__dirname, "../../../domain/commands") 12 | ], 13 | "eventsPath": [ 14 | path.join(__dirname, "../../../domain/events") 15 | ], 16 | "servicesPath": [ 17 | { 18 | path: path.join(__dirname, "../../../domain/services"), 19 | namespace: "domain.services" 20 | } 21 | ], 22 | "middlewaresPath": [ 23 | path.join(__dirname, "../../../domain/middlewares") 24 | ], 25 | "database": { 26 | myMongoDB: { 27 | type: 'mongodb', 28 | connectionString: 'mongodb://localhost:27017/mydb', 29 | }, 30 | myRedis: { 31 | type: 'redis', 32 | host: 'localhost', 33 | port: 6379, 34 | }, 35 | mySqlLite1: { 36 | type: 'sqlite', 37 | filename: 'db.sqlite1', 38 | }, 39 | mySqlLite2: { 40 | type: 'sqlite', 41 | filename: 'db.sqlite2', 42 | initialQuery: [` 43 | CREATE TABLE IF NOT EXISTS users ( 44 | id TEXT PRIMARY KEY, 45 | birthday_yyyy TEXT NOT NULL, 46 | birthday_mm TEXT NOT NULL, 47 | birthday_dd TEXT NOT NULL 48 | ); 49 | `, 50 | ` 51 | CREATE TABLE IF NOT EXISTS profiles ( 52 | userId TEXT PRIMARY KEY, 53 | firstName TEXT NOT NULL, 54 | lastName TEXT NOT NULL, 55 | email TEXT NOT NULL, 56 | FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE 57 | ); 58 | `], 59 | }, 60 | myMySQL: { 61 | type: 'mysql', 62 | host: 'localhost', 63 | user: 'root', 64 | password: '', 65 | database: 'mydb', 66 | }, 67 | 68 | }, 69 | "servers" : [ 70 | { 71 | "name": "ServerNumberOne", 72 | "host": "localhost", 73 | "port": 442, 74 | "type": "http", 75 | "ssl": true, 76 | }, 77 | { 78 | "name": "ServerNumberTwo", 79 | "host": "localhost", 80 | "port": 80, 81 | "type": "http", 82 | }, 83 | { 84 | "name": "ServerNumberThree", 85 | "host": "localhost", 86 | "port": 1000, 87 | "type": "http", 88 | }, 89 | { 90 | "name": "ServerHTTP3", 91 | "host": "localhost", 92 | "port": 90, 93 | "type": "quic", 94 | }, 95 | { 96 | "name": "ServerRPC", 97 | "host": "localhost", 98 | "port": 100, 99 | "type": "rpc", 100 | }, 101 | ] 102 | } -------------------------------------------------------------------------------- /src/config/environments/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /src/config/environments/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /src/config/environments/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const MainLauncher = require("./adapters/MainLauncher"); 2 | const Application = require("./application/Application"); 3 | const ConfigCenter = require("./config/ConfigCenter"); 4 | const { tools } = require("./utils/ToolManager"); 5 | 6 | class hex { 7 | static instance = null 8 | constructor(environmentPath = false) { 9 | this.config = ConfigCenter.getInstance(environmentPath).init(); 10 | this.launcher = new MainLauncher(); 11 | this.application = new Application(); 12 | } 13 | 14 | launch() { 15 | return new Promise((resolve, reject) => { 16 | this.launcher.start().then(() => { 17 | this.application.run(); 18 | tools.logger.info("hex is running"); 19 | resolve(true); 20 | }).catch((error) => { 21 | tools.logger.error('Error starting application:'); 22 | tools.logger.error(error); 23 | reject(error); 24 | }); 25 | }); 26 | } 27 | 28 | stop() { 29 | return new Promise((resolve, reject) => { 30 | this.launcher.stop().then(() => { 31 | tools.logger.info("[hex] stopped"); 32 | resolve(true); 33 | }).catch((error) => { 34 | tools.logger.error('Error stopping application'); 35 | tools.logger.error(error); 36 | reject(error); 37 | }); 38 | }); 39 | } 40 | } 41 | 42 | // ? If you reading this, I'm sorry for the mess, lol. 43 | // ? but i will be happy if you collaborate with me to make this project better. 44 | 45 | module.exports = hex; -------------------------------------------------------------------------------- /src/utils/Hash.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const CryptoJS = require('crypto-js'); 3 | 4 | const SECRET_KEY = process.env.SECRET_KEY || 'MEOWMEOW'; 5 | 6 | async function hashPassword(password) { 7 | const saltRounds = 10; 8 | return await bcrypt.hash(password, saltRounds); 9 | } 10 | 11 | async function validatePassword(password, hash) { 12 | return await bcrypt.compare(password, hash); 13 | } 14 | 15 | function encrypt(data) { 16 | return CryptoJS.AES.encrypt(data, SECRET_KEY).toString(); 17 | } 18 | 19 | function decrypt(encryptedData) { 20 | const bytes = CryptoJS.AES.decrypt(encryptedData, SECRET_KEY); 21 | return bytes.toString(CryptoJS.enc.Utf8); 22 | } 23 | function isValidHash(hash) { 24 | // CryptoJS-encrypted strings are typically Base64-encoded 25 | // and start with "U2FsdGVkX1" (which is the Base64 encoding of "Salted__") 26 | return typeof hash === 'string' && hash.startsWith('U2FsdGVkX1'); 27 | } 28 | 29 | hash = { 30 | hashPassword, 31 | validatePassword, 32 | encrypt, 33 | decrypt, 34 | isValidHash, 35 | } 36 | module.exports = hash; -------------------------------------------------------------------------------- /src/utils/Logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | constructor() { 3 | if (Logger.instance) { 4 | return Logger.instance; // Singleton pattern 5 | } 6 | 7 | this.logLevel = 'info'; // Default log level 8 | this.logLevels = ['debug', 'info', 'warn', 'error']; // Log level hierarchy 9 | Logger.instance = this; 10 | 11 | // Color map for log levels 12 | this.colorMap = { 13 | debug: '\x1b[36m', // Cyan 14 | info: '\x1b[32m', // Green 15 | warn: '\x1b[33m', // Yellow 16 | error: '\x1b[31m', // Red 17 | reset: '\x1b[0m' // Reset color 18 | }; 19 | 20 | } 21 | 22 | setLogLevel(level) { 23 | if (this.logLevels.includes(level)) { 24 | this.logLevel = level; 25 | } else { 26 | console.warn(`Invalid log level: ${level}`); 27 | } 28 | } 29 | 30 | log(level, message, meta = {}) { 31 | if (this.logLevels.indexOf(level) >= this.logLevels.indexOf(this.logLevel)) { 32 | const color = this.colorMap[level] || this.colorMap.reset; 33 | 34 | const formattedMessage = `${color}[${level.toUpperCase()}] ${message}${this.colorMap.reset}`; 35 | 36 | if (Object.keys(meta).length > 0) { 37 | console.log(`${formattedMessage}\n${color}↳ meta: ${typeof meta}`, `${JSON.stringify(meta, null, 2)}${this.colorMap.reset}`); 38 | } else { 39 | console.log(formattedMessage); 40 | } 41 | } 42 | } 43 | 44 | debug(message, meta = {}) { 45 | this.log('debug', message, meta); 46 | } 47 | 48 | info(message, meta = {}) { 49 | this.log('info', message, meta); 50 | } 51 | 52 | warn(message, meta = {}) { 53 | this.log('warn', message, meta); 54 | } 55 | 56 | error(message, meta = {}) { 57 | if (message instanceof Error) { 58 | this.log('error', message.message, message); 59 | } else { 60 | this.log('error', message, meta); 61 | } 62 | } 63 | } 64 | 65 | module.exports = Logger; -------------------------------------------------------------------------------- /src/utils/SessionManager.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid'); 2 | const SessionStorage = require('./SessionStorage'); 3 | const jwt = require('jsonwebtoken'); 4 | const { tools } = require('./ToolManager'); 5 | 6 | class SessionManager { 7 | #req; 8 | #res; 9 | #sessions; 10 | 11 | constructor(req, res) { 12 | this.#req = req; 13 | this.#res = res; 14 | this.#sessions = SessionStorage; 15 | } 16 | 17 | createSession(data, ttl = 3600, secure = false) { 18 | try { 19 | const sessionId = uuidv4(); 20 | this.#sessions.add(sessionId, data, ttl); 21 | let token; 22 | if (secure) { 23 | const expirationTime = Math.floor(Date.now() / 1000) + ttl; // 1 hour from now 24 | token = jwt.sign( 25 | { data: sessionId, exp: expirationTime }, // Add `exp` manually 26 | process.env.SECRET_KEY 27 | ); 28 | token = tools.hash.encrypt(token); 29 | } else { 30 | token = sessionId; 31 | } 32 | 33 | this.#setCookie("sessionId", token, { 34 | httpOnly: true, 35 | path: '/', 36 | sameSite: 'strict', 37 | // secure: true, // Uncomment if using HTTPS 38 | }); 39 | return token; 40 | } catch (error) { 41 | console.log('create cookie error', error); 42 | return false; 43 | } 44 | } 45 | 46 | getSession(secure = false) { 47 | try { 48 | let sessionId = this.#getCookie("sessionId"); 49 | if (secure) { 50 | if (!tools.hash.isValidHash(sessionId)) { 51 | return false; 52 | } 53 | sessionId = tools.hash.decrypt(sessionId); 54 | jwt.verify(sessionId, process.env.SECRET_KEY, (err, decoded) => { 55 | if (err) { 56 | console.error('Token verification failed:', err.message); 57 | return false; 58 | } else { 59 | sessionId = decoded.data; 60 | } 61 | }); 62 | } 63 | if (sessionId) { 64 | const data = this.#sessions.get(sessionId) || null; 65 | return { sessionId, data }; 66 | } 67 | } catch (error) { 68 | console.log('get cookie error', error); 69 | return false; 70 | } 71 | } 72 | 73 | destroySession(sessionId) { 74 | try { 75 | if (this.#sessions.has(sessionId)) { 76 | this.#sessions.drop(sessionId); 77 | this.#deleteCookie("sessionId", { path: '/' }); 78 | return true; 79 | } 80 | } catch (error) { 81 | console.log('destroy cookie error', error); 82 | return false; 83 | } 84 | } 85 | 86 | #setCookie(name, value, options = {}) { 87 | try { 88 | let cookieString = `${name}=${value}`; 89 | if (options.path) cookieString += `; Path=${options.path}`; 90 | if (options.httpOnly) cookieString += `; HttpOnly`; 91 | if (options.secure) cookieString += `; Secure`; 92 | if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`; 93 | if (options.expires) cookieString += `; Expires=${options.expires.toUTCString()}`; 94 | if (options.sameSite) cookieString += `; SameSite=${options.sameSite}`; 95 | this.#res.setHeader('Set-Cookie', cookieString); 96 | return true; 97 | } catch (error) { 98 | console.log('set cookie error', error); 99 | return false; 100 | } 101 | } 102 | 103 | #getCookie(name) { 104 | const cookies = this.#req.headers.cookie; 105 | if (!cookies) { 106 | return null; 107 | } 108 | const cookiePairs = cookies.split('; ').map(pair => pair.split('=')); 109 | const cookieObject = Object.fromEntries(cookiePairs); 110 | return cookieObject[name] || null; 111 | } 112 | 113 | #deleteCookie(name, options = {}) { 114 | this.#setCookie(name, "", { ...options, expires: new Date(0) }); 115 | } 116 | } 117 | 118 | module.exports = SessionManager; -------------------------------------------------------------------------------- /src/utils/SessionStorage.js: -------------------------------------------------------------------------------- 1 | class SessionStorage { 2 | constructor(gc = false) { 3 | if (SessionStorage.instance) { 4 | return SessionStorage.instance; 5 | } 6 | this.sessions = new Map(); 7 | this.expirationMap = new Map(); 8 | this.cleanupInterval = 60000; // 1 minute 9 | this.isCleanupRunning = false; 10 | SessionStorage.instance = this; 11 | if (gc) { 12 | this.startCleanup(); 13 | } 14 | } 15 | 16 | // Create or update a session 17 | add(sessionId, data, ttl = 3600) { 18 | const expiresAt = Date.now() + ttl * 1000; 19 | this.sessions.set(sessionId, { data, expiresAt }); 20 | 21 | if (!this.expirationMap.has(expiresAt)) { 22 | this.expirationMap.set(expiresAt, new Set()); 23 | } 24 | this.expirationMap.get(expiresAt).add(sessionId); 25 | } 26 | 27 | // Retrieve a session 28 | get(sessionId) { 29 | const session = this.sessions.get(sessionId); 30 | if (!session) return null; 31 | 32 | if (session.expiresAt < Date.now()) { 33 | this.drop(sessionId); 34 | return null; 35 | } 36 | 37 | return session.data; 38 | } 39 | 40 | // Delete a session 41 | drop(sessionId) { 42 | const session = this.sessions.get(sessionId); 43 | if (session) { 44 | this.expirationMap.get(session.expiresAt).delete(sessionId); 45 | if (this.expirationMap.get(session.expiresAt).size === 0) { 46 | this.expirationMap.delete(session.expiresAt); 47 | } 48 | this.sessions.delete(sessionId); 49 | } 50 | } 51 | 52 | // Clean up expired sessions 53 | cleanupExpiredSessions() { 54 | let expiredCount = 0; 55 | const now = Date.now(); 56 | const expirationTimes = Array.from(this.expirationMap.keys()).sort((a, b) => a - b); 57 | 58 | for (const expiresAt of expirationTimes) { 59 | if (expiresAt > now) break; // No need to check future expiration times 60 | 61 | const sessionIds = this.expirationMap.get(expiresAt); 62 | 63 | for (const sessionId of sessionIds) { 64 | expiredCount++; 65 | this.sessions.delete(sessionId); 66 | } 67 | this.expirationMap.delete(expiresAt); 68 | } 69 | console.log(`cleaning ${expiredCount} expired session`); 70 | 71 | } 72 | 73 | // Start the automated cleanup process 74 | startCleanup() { 75 | if (this.isCleanupRunning) return; 76 | this.isCleanupRunning = true; 77 | 78 | const cleanup = () => { 79 | setTimeout(() => { 80 | try { 81 | this.cleanupExpiredSessions(); 82 | } catch (e) { 83 | console.error('Cleanup error:', e); 84 | } 85 | cleanup(); 86 | }, this.cleanupInterval); 87 | }; 88 | 89 | cleanup(); 90 | } 91 | 92 | // Get all active sessions (for debugging or monitoring) 93 | getAll() { 94 | this.cleanupExpiredSessions(); // Clean up before returning 95 | return Array.from(this.sessions.entries()).map(([id, session]) => ({ 96 | id, 97 | data: session.data, 98 | expiresAt: new Date(session.expiresAt).toISOString(), 99 | })); 100 | } 101 | } 102 | 103 | module.exports = new SessionStorage(); -------------------------------------------------------------------------------- /src/utils/ToolManager.js: -------------------------------------------------------------------------------- 1 | const hash = require("./Hash"); 2 | const Logger = require("./Logger"); 3 | const tools = { 4 | logger: new Logger(), 5 | hash: hash, 6 | helper: { 7 | groupBy: (items, key) => { 8 | return items.reduce((grouped, item) => { 9 | const groupKey = item[key]; 10 | if (!grouped[groupKey]) { 11 | grouped[groupKey] = []; 12 | } 13 | grouped[groupKey].push(item); 14 | return grouped; 15 | }, {}); 16 | } 17 | 18 | }, 19 | } 20 | 21 | exports.tools = tools; -------------------------------------------------------------------------------- /src/v/command/command.v: -------------------------------------------------------------------------------- 1 | module command 2 | 3 | import commandparser { Request, ParsedCommand, parse } 4 | 5 | pub struct Command { 6 | pub mut: 7 | data map[string]string // Contains properties like protocol, method, target, etc. 8 | typ string // Command type (set during parsing) 9 | signature string // Computed pattern signature 10 | response string 11 | dispatcher string 12 | error string 13 | status_code int 14 | } 15 | 16 | pub fn new_command(request map[string]string) !Command { 17 | mut cmd := Command{} 18 | // Create a Request from the map. 19 | // (Assumes the request map has keys such as "req_type", "url", "method", etc.) 20 | req := Request{ 21 | req_type: request['req_type'] or { 'UNKNOWN' }, 22 | data: request, 23 | input_data: map[string]string{}, 24 | query_params: map[string]string{}, 25 | url: request['url'] or { '' }, 26 | method: request['method'] or { 'GET' }, 27 | headers: map[string]string{}, 28 | } 29 | parsed := parse(req)! 30 | // Use clone() to copy the map. 31 | cmd.data = parsed.data.clone() 32 | cmd.typ = parsed.typ 33 | return cmd 34 | } 35 | 36 | pub fn (mut cmd Command) pattern() string { 37 | cmd.signature = pattern_from_data(cmd.data) 38 | return cmd.signature 39 | } 40 | 41 | pub fn (mut cmd Command) set_session(session string) { 42 | cmd.data['session'] = session 43 | } 44 | 45 | pub fn (cmd Command) get_session() string { 46 | return cmd.data['session'] 47 | } 48 | 49 | pub fn (mut cmd Command) set_response(response string) { 50 | cmd.response = response 51 | } 52 | 53 | pub fn (mut cmd Command) set_dispatcher(dispatcher string) { 54 | cmd.dispatcher = dispatcher 55 | } 56 | 57 | pub fn (mut cmd Command) set_error(err string) { 58 | cmd.error = err 59 | } 60 | 61 | pub fn (mut cmd Command) set_status_code(status int) { 62 | cmd.status_code = status 63 | } 64 | 65 | pub fn pattern_from_data(data map[string]string) string { 66 | // Expected keys: type, protocol, method, target. 67 | // If a key is missing, a default value is used. 68 | typ := data['type'] or { 'UNKNOWN' } 69 | protocol := data['protocol'] or { 'UNKNOWN' } 70 | method := data['method'] or { 'UNKNOWN' } 71 | target := (data['target'] or { 'UNKNOWN' }).to_upper() 72 | return 'COMMAND:${typ}.${protocol}:${method}:${target}' 73 | } 74 | -------------------------------------------------------------------------------- /src/v/command/command_dispatcher.v: -------------------------------------------------------------------------------- 1 | module command_dispatcher 2 | 3 | import command 4 | import toolmanager 5 | 6 | // Remove the parameter name from the function pointer type. 7 | pub type HandlerFn = fn (mut command.Command, map[string]string) !string 8 | 9 | pub struct CommandHandler { 10 | pub mut: 11 | // Make the function pointer optional so it must be initialized. 12 | handler ?HandlerFn 13 | middlewares []HandlerFn // (For simplicity, middleware handling is omitted.) 14 | } 15 | 16 | pub struct CommandDispatcher { 17 | pub mut: 18 | handlers map[string]CommandHandler 19 | } 20 | 21 | pub fn new_dispatcher() &CommandDispatcher { 22 | return &CommandDispatcher{ 23 | handlers: map[string]CommandHandler{} 24 | } 25 | } 26 | 27 | pub fn (mut disp CommandDispatcher) register_command_handler(pattern string, handler CommandHandler) { 28 | if pattern in disp.handlers { 29 | toolmanager.logger.error("Handler for pattern $pattern is already registered") 30 | return 31 | } 32 | disp.handlers[pattern] = handler 33 | } 34 | 35 | pub fn (mut disp CommandDispatcher) dispatch_command(pattern string, payload map[string]string, mut cmd command.Command) !string { 36 | if pattern !in disp.handlers { 37 | return error("Handler for pattern $pattern not found") 38 | } 39 | mut handler := disp.handlers[pattern] 40 | // Unwrap the optional handler function pointer. 41 | f := handler.handler or { return error("Handler function not set for pattern $pattern") } 42 | result := f(mut cmd, payload)! 43 | return result 44 | } 45 | -------------------------------------------------------------------------------- /src/v/command/command_router.v: -------------------------------------------------------------------------------- 1 | module command_router 2 | 3 | import command_dispatcher 4 | import toolmanager 5 | 6 | pub struct CommandRouter { 7 | pub mut: 8 | dispatcher &command_dispatcher.CommandDispatcher 9 | } 10 | 11 | pub fn new_command_router(disp &command_dispatcher.CommandDispatcher) CommandRouter { 12 | return unsafe { 13 | CommandRouter{ 14 | dispatcher: disp 15 | } 16 | } 17 | } 18 | 19 | 20 | pub fn (mut router CommandRouter) register_command(pattern string, handler command_dispatcher.CommandHandler) { 21 | router.dispatcher.register_command_handler(pattern, handler) 22 | toolmanager.logger.info("Registered command: " + pattern) 23 | } 24 | -------------------------------------------------------------------------------- /src/v/command/commandparser.v: -------------------------------------------------------------------------------- 1 | module commandparser 2 | 3 | import toolmanager 4 | 5 | pub struct Request { 6 | pub: 7 | req_type string // e.g. "HTTP", "RPC", etc. 8 | data map[string]string // additional request data 9 | input_data map[string]string 10 | query_params map[string]string 11 | url string 12 | method string 13 | headers map[string]string 14 | } 15 | 16 | pub struct ParsedCommand { 17 | pub: 18 | typ string // e.g. "REQUEST" or "UNKNOWN" 19 | data map[string]string // should contain keys like protocol, method, target, etc. 20 | } 21 | 22 | pub fn parse(req Request) !ParsedCommand { 23 | mut command_data := map[string]string{} 24 | mut typ := '' 25 | match req.req_type { 26 | 'HTTP', 'HTTPS' { 27 | command_data = http_request_to_command(req)! 28 | typ = 'REQUEST' 29 | } 30 | 'RPC' { 31 | command_data = rpc_request_to_command(req)! 32 | typ = 'REQUEST' 33 | } 34 | else { 35 | command_data = generic_request_to_command(req)! 36 | typ = 'UNKNOWN' 37 | toolmanager.logger.error('unsupported request type: ' + req.req_type) 38 | } 39 | } 40 | // Save the type into the command data so that later the pattern can be computed. 41 | command_data['type'] = typ 42 | return ParsedCommand{ 43 | typ: typ, 44 | data: command_data 45 | } 46 | } 47 | 48 | fn http_request_to_command(req Request) !map[string]string { 49 | mut command_data := map[string]string{} 50 | command_data['protocol'] = req.req_type.to_upper() 51 | command_data['method'] = req.method.to_upper() 52 | // Propagate error from validate_route using ! 53 | command_data['target'] = validate_route(req.url)! 54 | return command_data 55 | } 56 | 57 | fn rpc_request_to_command(req Request) !map[string]string { 58 | mut command_data := map[string]string{} 59 | if 'serviceName' in req.data && 'methodName' in req.data { 60 | command_data['protocol'] = 'RPC' 61 | command_data['method'] = req.data['methodName'] 62 | command_data['target'] = req.data['serviceName'] + '/' + req.data['methodName'] 63 | } else { 64 | return error('Invalid RPC request') 65 | } 66 | return command_data 67 | } 68 | 69 | fn generic_request_to_command(req Request) !map[string]string { 70 | mut command_data := map[string]string{} 71 | command_data['protocol'] = req.req_type.to_upper() 72 | command_data['method'] = 'UNKNOWN' 73 | command_data['target'] = 'UNKNOWN' 74 | return command_data 75 | } 76 | 77 | fn validate_route(route string) !string { 78 | if route.len == 0 { 79 | return error('Invalid route') 80 | } 81 | return route.to_upper() 82 | } 83 | -------------------------------------------------------------------------------- /src/v/command/main.v: -------------------------------------------------------------------------------- 1 | import command 2 | import command_dispatcher 3 | import command_router 4 | import toolmanager 5 | 6 | fn dummy_handler(mut cmd command.Command, payload map[string]string) !string { 7 | // Simulate command handling logic. 8 | toolmanager.logger.info("Executing dummy handler for command: " + cmd.signature) 9 | cmd.set_response("Response from dummy handler") 10 | return "Handler executed successfully" 11 | } 12 | 13 | fn main() { 14 | // Simulate an incoming request as a map. 15 | request := { 16 | 'req_type': 'HTTP' 17 | 'url': '/api/test' 18 | 'method': 'GET' 19 | } 20 | 21 | // Create a new command from the request. 22 | mut cmd := command.new_command(request) or { 23 | toolmanager.logger.error('Failed to create command: $err') 24 | return 25 | } 26 | 27 | // Compute the command pattern. 28 | pattern := cmd.pattern() 29 | toolmanager.logger.info("Command pattern: " + pattern) 30 | 31 | // Create a dispatcher (heap-allocated) and a router. 32 | mut dispatcher := command_dispatcher.new_dispatcher() 33 | mut router := command_router.new_command_router(dispatcher) 34 | 35 | // Create a command handler using the dummy handler function. 36 | handler := command_dispatcher.CommandHandler{ 37 | handler: dummy_handler 38 | middlewares: []command_dispatcher.HandlerFn{} // no middleware in this simple example 39 | } 40 | 41 | // Register the command handler with the router. 42 | router.register_command(pattern, handler) 43 | 44 | // Dispatch the command (with an empty payload in this example). 45 | result := dispatcher.dispatch_command(pattern, map[string]string{}, mut cmd) or { 46 | toolmanager.logger.error("Dispatch error: $err") 47 | return 48 | } 49 | toolmanager.logger.info("Dispatch result: " + result) 50 | } 51 | -------------------------------------------------------------------------------- /src/v/command/toolmanager.v: -------------------------------------------------------------------------------- 1 | module toolmanager 2 | 3 | pub struct Logger {} 4 | 5 | pub fn (l Logger) error(msg string) { 6 | println('[ERROR] ' + msg) 7 | } 8 | 9 | pub fn (l Logger) warn(msg string) { 10 | println('[WARN] ' + msg) 11 | } 12 | 13 | pub fn (l Logger) info(msg string) { 14 | println('[INFO] ' + msg) 15 | } 16 | 17 | pub const logger = Logger{} 18 | --------------------------------------------------------------------------------