├── src ├── common │ ├── exceptions │ │ ├── index.ts │ │ ├── type.d.ts │ │ └── http.exception.ts │ ├── type.d.ts │ ├── decorators │ │ ├── index.ts │ │ ├── common │ │ │ ├── type.d.ts │ │ │ └── index.ts │ │ ├── type.d.ts │ │ ├── validate │ │ │ └── index.ts │ │ ├── http │ │ │ └── index.ts │ │ └── socket │ │ │ └── index.ts │ ├── index.ts │ ├── tsconfig.json │ ├── tsconfig.build.json │ └── request │ │ ├── validations.ts │ │ ├── index.ts │ │ └── type.d.ts ├── index.ts ├── type.d.ts ├── core │ ├── index.ts │ ├── type.d.ts │ ├── cache │ │ ├── type.d.ts │ │ └── index.ts │ ├── elumian │ │ └── index.ts │ ├── tsconfig.json │ ├── server │ │ ├── type.d.ts │ │ └── index.ts │ ├── tsconfig.build.json │ └── crypto │ │ └── index.ts ├── tsconfig.json └── tsconfig.build.json ├── .env.example ├── pnpm-workspace.yaml ├── .gitignore ├── test ├── http │ ├── validations.test.ts │ └── index.test.ts ├── cache │ ├── types.d.ts │ ├── crearType.test.ts │ └── index.test.ts ├── validations │ └── index.test.ts └── crypto │ └── index.test.ts ├── docker-compose.yml ├── biome.json ├── .github └── dependabot.yml ├── LICENSE ├── tsconfig.json ├── package.json └── README.md /src/common/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./http.exception" 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core"; 2 | export * from "./common"; 3 | -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./core/type"; 2 | export * from "./common/type"; 3 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | export * from "./elumian" 3 | export * from "./server" 4 | -------------------------------------------------------------------------------- /src/common/type.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./request/type"; 2 | export * from "./decorators/type"; 3 | export * from "./exceptions/type"; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SERVER_PORT = 5000 2 | 3 | JWT_TOKEN = '' 4 | eln_SECRET_KEY = "" 5 | 6 | DATABASE_URL="" 7 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@prisma/client' 3 | - '@prisma/engines' 4 | - bcrypt 5 | - esbuild 6 | - prisma 7 | -------------------------------------------------------------------------------- /src/core/type.d.ts: -------------------------------------------------------------------------------- 1 | export type elumian = Record; 2 | 3 | export * from "./cache/type"; 4 | export * from "./server/type"; 5 | -------------------------------------------------------------------------------- /src/common/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./http" 2 | export * from "./common" 3 | export * from "./socket" 4 | export * from "./validate" 5 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | export * from "./decorators" 3 | export * from "./exceptions/" 4 | export * from "./request" 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/common/decorators/common/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface moduleMetadata { 2 | controllers: Array, 3 | services?: Array, 4 | middlewares?: Array 5 | } 6 | -------------------------------------------------------------------------------- /src/core/cache/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface cacheData { 2 | id: string; 3 | data: string; 4 | expireTime?: Date; 5 | } 6 | 7 | export type cacheLists = Record; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .git 4 | 5 | .env 6 | 7 | dist 8 | 9 | .vscode 10 | 11 | lib/ 12 | 13 | example/ 14 | 15 | yarn-error.log 16 | 17 | yarn.lock 18 | 19 | pnpm-lock.yaml 20 | -------------------------------------------------------------------------------- /src/common/exceptions/type.d.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from "./http.exception"; 2 | export interface HttpExceptionOptions { 3 | status: HttpStatus, 4 | message: any, 5 | type: 'WARNING' | 'INFO' | 'DANGER' | 'SUCCESS' 6 | } 7 | -------------------------------------------------------------------------------- /src/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node" 6 | ] 7 | }, 8 | "files": [], 9 | "include": [], 10 | "references": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/core/elumian/index.ts: -------------------------------------------------------------------------------- 1 | import * as Cache from "../cache"; 2 | import * as Crypto from "../crypto"; 3 | import { elumian } from "../type"; 4 | 5 | export const Elumian: elumian = { 6 | cache: Cache, 7 | crypto: Crypto, 8 | }; 9 | -------------------------------------------------------------------------------- /src/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.build.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/http/validations.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { validations, setMessages } from "elumian/core/request" 3 | setMessages({ required: ' es requerido' }) 4 | const validate = validations.compareData({ piel: "uss", perro: "123" }, { perro: ['numeric'], piel: ['required'] }) 5 | console.log(validate) 6 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [ 4 | "./index.ts", 5 | "./type.d.ts" 6 | ], 7 | "compilerOptions": { 8 | "outDir": "../lib", 9 | }, 10 | "references": [ 11 | { 12 | "path": "./common/tsconfig.build.json" 13 | }, 14 | { 15 | "path": "./core/tsconfig.build.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/cache/types.d.ts: -------------------------------------------------------------------------------- 1 | type FuncType = (...args: any[]) => any; 2 | 3 | type SubArray1 = [{ Auth: any[] }, (arg0: any) => any, (arg0: any, arg1: any) => any, (arg0: any, arg1: any) => any]; 4 | type SubArray2 = [() => any, (arg0: any) => any, (arg0: any) => any, (arg0: any) => any, (arg0: any) => any, (arg0: any) => any, (arg0: any) => any]; 5 | 6 | type MainArray = [SubArray1, SubArray2]; 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | postgres: 4 | image: postgres 5 | container_name: elumian-test 6 | environment: 7 | - POSTGRES_USER=elumian 8 | - POSTGRES_PASSWORD=elumian 9 | - POSTGRES_DB=elumian 10 | ports: 11 | - 5432:5432 12 | volumes: 13 | - pgdata:/var/lib/postgresql/data 14 | 15 | volumes: 16 | pgdata: 17 | -------------------------------------------------------------------------------- /test/validations/index.test.ts: -------------------------------------------------------------------------------- 1 | import { validations } from "elumian/common/request"; 2 | console.log( 3 | validations.compareData( 4 | { 5 | username: "krashmello", 6 | password: "1234", 7 | email: "krashmello@gmail.com", 8 | }, 9 | { 10 | username: ["alpha", "min:3", "max:20"], 11 | password: ["alphaNumeric", "min:6", "max:20"], 12 | email: ["email"], 13 | }, 14 | ), 15 | ); 16 | -------------------------------------------------------------------------------- /src/core/server/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface ServerConfig { 2 | port?: number; 3 | } 4 | export interface RouteInfo { 5 | controller: string; 6 | method: string; 7 | path: string; 8 | } 9 | 10 | export interface SocketRoute { 11 | method: string; 12 | path: string; 13 | functionSocket: (io: any, socket: any) => void; 14 | } 15 | 16 | export type SocketInfo = { method: string; path: string; handlerName: string }; 17 | -------------------------------------------------------------------------------- /test/crypto/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Elumian } from "elumian/core"; 2 | let encrypted = Elumian.crypto.hardEncrypt({ id: 1, name: "asd" }); 3 | console.log(encrypted); 4 | console.log(Elumian.crypto.hardDecrypt(encrypted)); 5 | console.log( 6 | Elumian.crypto.hardDecrypt( 7 | "cgtzG1lTxwn8Ha.ijxJ5Ohx6sL1/DdAReuWB/1u264zhb/iBYoVoTdrwHDINWBZKEKrCwyzEC/XL9RZO7xVcxHuy2HQOpaAWONaZUA1B33dmTeU2fqsJw8snmE=", 8 | true, 9 | ), 10 | ); 11 | -------------------------------------------------------------------------------- /src/common/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "../../lib/common/", 5 | "rootDir": ".", 6 | "paths": { 7 | "elumian/core": [ 8 | "../core" 9 | ], 10 | "elumian/core/*": [ 11 | "../core/*" 12 | ], 13 | } 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dist", 18 | "test/**/*", 19 | "*.spec.ts" 20 | ], 21 | "references": [] 22 | } 23 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false 10 | }, 11 | "formatter": { 12 | "enabled": true, 13 | "indentStyle": "tab" 14 | }, 15 | "linter": { 16 | "enabled": true, 17 | "rules": { 18 | "recommended": true 19 | } 20 | }, 21 | "javascript": { 22 | "formatter": { 23 | "quoteStyle": "double" 24 | } 25 | }, 26 | "assist": { 27 | "enabled": true, 28 | "actions": { 29 | "source": { 30 | "organizeImports": "on" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "skipLibCheck": true, 8 | "noUnusedLocals": false, 9 | "importHelpers": true, 10 | "removeComments": false, 11 | "noLib": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "useUnknownInCatchVariables": false, 15 | "target": "ESNext", 16 | "sourceMap": false, 17 | "allowJs": false, 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "types": [ 21 | "node" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | version: 2 6 | updates: 7 | # Enable version updates for npm 8 | - package-ecosystem: "npm" 9 | # Look for `package.json` and `lock` files in the `root` directory 10 | directory: "/" 11 | # Check the npm registry for updates every day (weekdays) 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /src/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "../../lib/core/", 5 | "rootDir": ".", 6 | "paths": { 7 | "elumian/core": [ 8 | "../" 9 | ], 10 | "elumian/core/*": [ 11 | "../*" 12 | ], 13 | "elumian/common": [ 14 | "../common" 15 | ], 16 | "elumian/common/*": [ 17 | "../common/*" 18 | ] 19 | } 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "dist", 24 | "test/**/*", 25 | "*.spec.ts" 26 | ], 27 | "references": [ 28 | { 29 | "path": "../common/tsconfig.build.json" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/common/decorators/type.d.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export type Methods = "get" | "post" | "delete" | "path" | "options" | "put"; 4 | export type MethodsSocket = "on" | "emit"; 5 | export type tGuard = (req: Request, res: Response, next: NextFunction) => any; 6 | export interface IRouter { 7 | method: Methods; 8 | path: string; 9 | isProtected: boolean; 10 | handlerName: string; 11 | guard?: tGuard[]; 12 | } 13 | export interface SRouter { 14 | method: MethodsSocket; 15 | pathName: string; 16 | handlerName: string; 17 | } 18 | export interface ControllerType { 19 | functionController; 20 | } 21 | export * from "./common/type"; 22 | -------------------------------------------------------------------------------- /src/common/exceptions/http.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpExceptionOptions } from "./type"; 2 | 3 | export enum HttpStatus { 4 | created = 201, 5 | ok = 200, 6 | noContent = 204, 7 | badRequest = 400, 8 | unauthorized = 401, 9 | forbidden = 403, 10 | notFound = 404, 11 | conflict = 409, 12 | internalServerError = 500, 13 | notImplemented = 501, 14 | badGateway = 502, 15 | serviceUnavailable = 503, 16 | } 17 | 18 | export const HttpExceptions = (opt: HttpExceptionOptions) => { 19 | let { status, message, type } = opt; 20 | status = status || HttpStatus.internalServerError; 21 | type = type || "DANGER"; 22 | console.error(`${type}: ${JSON.stringify(message)}`); 23 | throw { 24 | status, 25 | data: { 26 | message, 27 | type, 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/common/decorators/validate/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpExceptions } from "../../exceptions"; 2 | import { validations, setMessages } from "../../request"; 3 | import type { 4 | validationsOptions, 5 | validationsMessage, 6 | } from "../../request/type"; 7 | 8 | const validateMapping = 9 | (type: string) => 10 | (dataValidations: validationsOptions, messages?: validationsMessage) => { 11 | return (target, propertyKey, descriptor) => { 12 | const method = descriptor.value; 13 | descriptor.value = async function () { 14 | const [req, res] = arguments; 15 | if (messages) setMessages(messages); 16 | const errors = validations.compareData(req[type], dataValidations); 17 | if (errors !== true) { 18 | HttpExceptions({ 19 | status: 401, 20 | message: errors, 21 | type: "WARNING", 22 | }); 23 | return; 24 | } 25 | return method.apply(this, arguments); 26 | }; 27 | }; 28 | }; 29 | export const ValidateBody = validateMapping("body"); 30 | export const ValidateQuery = validateMapping("query"); 31 | export const ValidateParams = validateMapping("params"); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 KrashMello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/common/decorators/http/index.ts: -------------------------------------------------------------------------------- 1 | enum RequestMethod { 2 | GET = "get", 3 | POST = "post", 4 | PUT = "put", 5 | DELETE = "delete", 6 | PATH = "path", 7 | } 8 | interface IRequestMapping { 9 | path: string; 10 | method: RequestMethod; 11 | } 12 | 13 | const createRequestMapping = (method: RequestMethod) => (path?: string) => 14 | requestMapping({ path, method }); 15 | const requestMapping = (metadata: IRequestMapping) => { 16 | let { path, method } = metadata; 17 | path = path && path.length ? path : "/"; 18 | method = method || RequestMethod.GET; 19 | return (target, propertyKey, descriptor) => { 20 | Reflect.defineMetadata("method", method, descriptor.value); 21 | Reflect.defineMetadata("path", path, descriptor.value); 22 | Reflect.defineMetadata("handlerName", descriptor.name, descriptor.value); 23 | return descriptor; 24 | }; 25 | }; 26 | export const Get = createRequestMapping(RequestMethod.GET); 27 | export const Post = createRequestMapping(RequestMethod.POST); 28 | export const Put = createRequestMapping(RequestMethod.PUT); 29 | export const Delete = createRequestMapping(RequestMethod.DELETE); 30 | export const Path = createRequestMapping(RequestMethod.PATH); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "noUnusedLocals": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "useUnknownInCatchVariables": false, 11 | "target": "ES2021", 12 | "sourceMap": true, 13 | "allowJs": false, 14 | "outDir": "lib", 15 | "baseUrl": ".", 16 | "paths": { 17 | "elumian": [ 18 | "src" 19 | ], 20 | "elumian/*": [ 21 | "src/*" 22 | ], 23 | "elumian/core": [ 24 | "src/core" 25 | ], 26 | "elumian/core/*": [ 27 | "src/core/*" 28 | ], 29 | "elumian/common": [ 30 | "src/common" 31 | ], 32 | "elumian/common/*": [ 33 | "src/common/*" 34 | ], 35 | }, 36 | "types": [ 37 | "./src/type.d.ts", 38 | "node" 39 | ] 40 | }, 41 | "include": [ 42 | "./src/**/*", 43 | ], 44 | "exclude": [ 45 | "node_modules", 46 | "lib", 47 | "example", 48 | "**/**.test.ts" 49 | ], //production 50 | // "exclude": ["node_modules", "lib"], // development 51 | "ts-node": { 52 | "require": [ 53 | "tsconfig-paths/register" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/cache/crearType.test.ts: -------------------------------------------------------------------------------- 1 | // import fs from "node:fs"; 2 | import { Elumian } from "elumian/core"; 3 | import * as fs from "node:fs"; 4 | const keys = Object.keys(Elumian); 5 | const descriptors = Object.getOwnPropertyDescriptors(Elumian); 6 | const types = keys.map((value) => { 7 | const valueDescriptors = Object.getOwnPropertyDescriptors( 8 | descriptors[value].value, 9 | ); 10 | const valueDescriptorsKeys = Object.keys(valueDescriptors).filter( 11 | (k) => k !== "__esModule", 12 | ); 13 | return valueDescriptorsKeys.map((valuesDescriptorKey) => { 14 | return valueDescriptors[valuesDescriptorKey].value; 15 | }); 16 | }); 17 | function getType(value) { 18 | if (typeof value === "function") { 19 | return getFunctionType(value); 20 | } 21 | if (typeof value === "object") { 22 | if (Array.isArray(value)) return "any[]"; 23 | else { 24 | // Simple parser para objeto con propiedades 25 | let props = Object.entries(value) 26 | .map(([k, v]) => `${k}: any[]`) 27 | .join("; "); 28 | return `{ ${props} }`; 29 | } 30 | } 31 | return "any"; 32 | } 33 | 34 | function generateTypeScript(arr) { 35 | let result = "type FuncType = (...args: any[]) => any;\n\n"; 36 | 37 | arr.forEach((subArr, i) => { 38 | const types = subArr.map(getType); 39 | result += `type SubArray${i + 1} = [${types.join(", ")}];\n`; 40 | }); 41 | 42 | result += `\ntype MainArray = [${arr.map((_, i) => `SubArray${i + 1}`).join(", ")}];\n`; 43 | 44 | return result; 45 | } 46 | function getFunctionType(func) { 47 | const paramCount = func.length; 48 | const params = []; 49 | for (let i = 0; i < paramCount; i++) { 50 | params.push(`arg${i}: any`); 51 | } 52 | return `(${params.join(", ")}) => any`; 53 | } 54 | const tsOutput = generateTypeScript(types); 55 | console.log(tsOutput); 56 | import * as path from "node:path"; 57 | fs.writeFileSync(path.join(__dirname, "types.d.ts"), tsOutput); 58 | -------------------------------------------------------------------------------- /src/common/decorators/socket/index.ts: -------------------------------------------------------------------------------- 1 | import { SRouter } from "../type"; 2 | 3 | export const socketOn = (pathName: string): MethodDecorator => { 4 | return (target, propertyKey) => { 5 | if (!Reflect.hasMetadata("routesSocket", target.constructor)) { 6 | Reflect.defineMetadata("routesSocket", [], target.constructor); 7 | } 8 | const routes: SRouter[] = Reflect.getMetadata( 9 | "routesSocket", 10 | target.constructor, 11 | ); 12 | const index = routes.findIndex( 13 | (i) => i.handlerName === (propertyKey as string), 14 | ); 15 | 16 | if ( 17 | routes.filter((r) => r.handlerName === (propertyKey as string)).length === 18 | 0 19 | ) 20 | routes.push({ 21 | method: "on", 22 | pathName, 23 | handlerName: propertyKey as string, 24 | }); 25 | else 26 | routes[index] = { 27 | method: "on", 28 | pathName, 29 | handlerName: propertyKey as string, 30 | }; 31 | }; 32 | }; 33 | 34 | export const socketEmit = (pathName: string): MethodDecorator => { 35 | return (target, propertyKey) => { 36 | if (!Reflect.hasMetadata("routesSocket", target.constructor)) { 37 | Reflect.defineMetadata("routesSocket", [], target.constructor); 38 | } 39 | const routes: SRouter[] = Reflect.getMetadata( 40 | "routesSocket", 41 | target.constructor, 42 | ); 43 | const index = routes.findIndex( 44 | (i) => i.handlerName === (propertyKey as string), 45 | ); 46 | 47 | if ( 48 | routes.filter((r) => r.handlerName === (propertyKey as string)).length === 49 | 0 50 | ) 51 | routes.push({ 52 | method: "emit", 53 | pathName, 54 | handlerName: propertyKey as string, 55 | }); 56 | else 57 | routes[index] = { 58 | method: "emit", 59 | pathName, 60 | handlerName: propertyKey as string, 61 | }; 62 | }; 63 | }; 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/common/decorators/common/index.ts: -------------------------------------------------------------------------------- 1 | import { moduleMetadata } from "./type"; 2 | export const Middleware = (target) => { 3 | const descriptors = Object.getOwnPropertyDescriptors(target.prototype); 4 | if (descriptors.init) 5 | Reflect.defineMetadata("middleware", descriptors.init, target); 6 | return target; 7 | }; 8 | const reflectorCreate = 9 | (key: string, value: any): MethodDecorator => 10 | (target, _propertyKey, descriptor) => { 11 | if (descriptor) { 12 | Reflect.defineMetadata(key, value, descriptor.value); 13 | return descriptor; 14 | } 15 | Reflect.defineMetadata(key, value, target); 16 | return target; 17 | }; 18 | export const Module = (metadata: moduleMetadata) => { 19 | let { controllers, services, middlewares } = metadata; 20 | middlewares = middlewares || []; 21 | controllers = controllers || []; 22 | services = services || []; 23 | return (target) => { 24 | Reflect.defineMetadata("controllers", controllers, target); 25 | Reflect.defineMetadata("middlewares", middlewares, target); 26 | Reflect.defineMetadata("services", services, target); 27 | return target; 28 | }; 29 | }; 30 | export const Controller = (prefix: string) => { 31 | return (constructor: T) => { 32 | Reflect.defineMetadata("prefix", prefix, constructor); 33 | Reflect.defineMetadata("handlerName", constructor.name, constructor); 34 | }; 35 | }; 36 | export const Service = () => { 37 | return (constructor: T) => { 38 | Reflect.defineMetadata("prefix", constructor.name, constructor); 39 | return class extends constructor { 40 | static instance; 41 | constructor(...args) { 42 | super(...args); 43 | } 44 | static getInstance() { 45 | if (!this.instance) { 46 | const dependencies = 47 | Reflect.getMetadata("design:paramtypes", constructor) || []; 48 | const instances = dependencies.map((dep) => dep.getInstance()); 49 | this.instance = new constructor(...instances); 50 | } 51 | return this.instance; 52 | } 53 | }; 54 | }; 55 | }; 56 | export const Public = reflectorCreate("isPublic", true); 57 | -------------------------------------------------------------------------------- /test/http/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Service, 3 | ValidateQuery, 4 | ValidateParams, 5 | ValidateBody, 6 | Middleware, 7 | Module, 8 | Post, 9 | Get, 10 | Public, 11 | Controller, 12 | } from "elumian/common"; 13 | import { validationsOptions } from "elumian/type"; 14 | import { HttpExceptions, HttpStatus } from "elumian/common"; 15 | import { Elumian, Server } from "elumian/core"; 16 | 17 | const bodyDataValidate: validationsOptions = { 18 | fecha: ["required", "date"], 19 | algo: ["numeric"], 20 | }; 21 | 22 | @Service() 23 | class User { 24 | getUser() { 25 | return "user"; 26 | } 27 | } 28 | 29 | @Service() 30 | class Personas { 31 | constructor(private user: User) {} 32 | async message(): Promise<{ 33 | status: HttpStatus; 34 | message: any; 35 | type: "INFO" | "SUCCESS" | "DANGER" | "WARNING"; 36 | }> { 37 | console.log(Elumian.User.getUser()); 38 | return { 39 | status: HttpStatus.ok, 40 | message: this.user.getUser(), 41 | type: "INFO", 42 | }; 43 | } 44 | } 45 | @Middleware 46 | class GlobalGuard { 47 | init(context) { 48 | const { handler } = context; 49 | const isPublic = Reflect.getMetadata("isPublic", handler); 50 | if (isPublic) return true; 51 | else 52 | HttpExceptions({ 53 | status: HttpStatus.forbidden, 54 | message: "No tienes permisos para acceder a esta ruta", 55 | type: "DANGER", 56 | }); 57 | return false; 58 | } 59 | } 60 | @Controller("test") 61 | class Test { 62 | constructor(private personas: Personas) {} 63 | @Get("/") 64 | @Public 65 | async test(req, res) { 66 | HttpExceptions(await this.personas.message()); 67 | } 68 | @Post("/us/1") 69 | @Public 70 | @ValidateBody(bodyDataValidate) 71 | @ValidateQuery(bodyDataValidate) 72 | test1(req, res) { 73 | HttpExceptions({ 74 | status: HttpStatus.ok, 75 | message: { test: "test" }, 76 | type: "SUCCESS", 77 | }); 78 | } 79 | @Post("/:id") 80 | @ValidateParams(bodyDataValidate) 81 | @ValidateBody(bodyDataValidate) 82 | test2(req, res) { 83 | HttpExceptions({ 84 | status: HttpStatus.ok, 85 | message: { test: "test" }, 86 | type: "SUCCESS", 87 | }); 88 | } 89 | } 90 | @Module({ 91 | controllers: [Test], 92 | services: [Personas], 93 | middlewares: [GlobalGuard], 94 | }) 95 | class asdf {} 96 | Server.setConfig({ port: 5000 }); 97 | Server.chargeModules([asdf]); 98 | Server.start(); 99 | -------------------------------------------------------------------------------- /test/cache/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Elumian } from "elumian/core"; 2 | const redisConfirmation = async () => { 3 | console.log("--------------\n\tRedis confirmation"); 4 | Elumian.cache.setConfigProvider({ 5 | url: "redis://:@localhost:6379", 6 | }); 7 | const cache = await Elumian.cache.singData({ 8 | key: "test", 9 | data: { id: 1, name: "asd" }, 10 | }); 11 | console.log(cache); 12 | console.log(await Elumian.cache.getData("test", cache.id)); 13 | console.log(Elumian.cache.list); 14 | console.log("----------\n\tConfirm\n"); 15 | }; 16 | const deleteConfirmation = async () => { 17 | console.log("--------------\nDelete Confirmation\n"); 18 | console.log('add in key: "test" new value\n with ttl: 10 seconds'); 19 | const cache = await Elumian.cache.singData({ 20 | key: "test", 21 | data: { id: 1, name: "asd" }, 22 | ttl: 10, 23 | }); 24 | console.log(`cache provide field: ${cache.id}\n`); 25 | console.log(`list caching \n`); 26 | console.log(Elumian.cache.list); 27 | setTimeout(() => { 28 | console.log("--------------\nDelete Confirmation\n"); 29 | console.log(`list without 10 secords field: ${cache.id} \n`); 30 | console.log(Elumian.cache.list); 31 | console.log("--------\nconfirm \n"); 32 | }, 10000); 33 | }; 34 | const verifyIdConfirmation = async () => { 35 | console.log("--------------\nverify id Confirmation\n"); 36 | console.log("add in key: test new value\n"); 37 | const cache = await Elumian.cache.singData({ 38 | key: "test", 39 | data: { id: 1, name: "asd" }, 40 | encrypted: true, 41 | }); 42 | console.log(`cache provide field: ${cache.id}\n`); 43 | console.log( 44 | `veryfy if field: "ramdom" in key: "test" exist: ${await Elumian.cache.verifyId("test", "random")} \n`, 45 | ); 46 | console.log( 47 | `veryfy if field: ${cache.id} in key: \"test\" exist: ${await Elumian.cache.verifyId("test", "random")}\n`, 48 | ); 49 | }; 50 | const verifyCustomFieldConfirmation = async () => { 51 | console.log("--------------\nverify custom field Confirmation\n"); 52 | console.log("add in key: test new value with a custom field name\n"); 53 | const cache = await Elumian.cache.singData({ 54 | key: "test", 55 | field: "random", 56 | data: { id: 1, name: "asd" }, 57 | encrypted: true, 58 | }); 59 | console.log(`cache provide field: ${cache.id}\n`); 60 | console.log( 61 | `veryfy if field custom in key: "test" exist: ${await Elumian.cache.verifyId("test", cache.id)} \n`, 62 | ); 63 | }; 64 | const test = async () => { 65 | await deleteConfirmation(); 66 | await verifyIdConfirmation(); 67 | await verifyCustomFieldConfirmation(); 68 | }; 69 | test(); 70 | -------------------------------------------------------------------------------- /src/core/cache/index.ts: -------------------------------------------------------------------------------- 1 | import { type cacheData, type cacheLists } from "./type"; 2 | import { Elumian } from ".."; 3 | import { createClient } from "redis"; 4 | 5 | const secondsToMidnight = (n: Date): number => { 6 | return ( 7 | (24 - n.getHours() - 1) * 60 * 60 + 8 | (60 - n.getMinutes() - 1) * 60 + 9 | (60 - n.getSeconds()) 10 | ); 11 | }; 12 | 13 | export const list: Record> = {}; 14 | 15 | let redisConfiguration: { 16 | url: string; 17 | }; 18 | 19 | const expireTime = (seconds: number = 1) => { 20 | return 1000 * seconds; 21 | }; 22 | 23 | export const setConfigProvider = (args: { url: string }) => { 24 | redisConfiguration = args; 25 | }; 26 | 27 | export const singData = async (args: { 28 | key: string; 29 | data: any; 30 | field?: string; 31 | encrypted?: { 32 | unsafe: boolean; 33 | }; 34 | ttl?: number; 35 | }): Promise => { 36 | let { key, data, field, encrypted, ttl } = args; 37 | 38 | const id = field || Elumian.crypto.codeGen(72); 39 | let result: { id: string; expireTime?: any } = { 40 | id, 41 | }; 42 | if (encrypted) { 43 | data = Elumian.crypto.hardEncrypt(data, encrypted.unsafe); 44 | } 45 | if (ttl) { 46 | const expirationDuration = secondsToMidnight(new Date()) * expireTime(ttl); 47 | result.expireTime = new Date(Date.now() + expirationDuration); 48 | } 49 | if (redisConfiguration) { 50 | const client = await createClient(redisConfiguration) 51 | .on("error", (err) => console.log("Redis Client Error", err)) 52 | .connect(); 53 | await client.hSet(key, id, JSON.stringify(data)); 54 | if (ttl) await client.hExpire(key, id, ttl || 60); 55 | client.destroy(); 56 | } else { 57 | if (!list[key]) list[key] = {}; 58 | if (!list[key][id]) list[key][id] = data; 59 | if (ttl) { 60 | setTimeout(() => { 61 | delete list[key][id]; 62 | }, expireTime(ttl)); 63 | } 64 | } 65 | return result; 66 | }; 67 | 68 | export const getData = async ( 69 | key: string, 70 | field: string, 71 | ): Promise => { 72 | let value: string | Record; 73 | if (redisConfiguration) { 74 | const client = await createClient(redisConfiguration) 75 | .on("error", (err) => console.log("Redis Client Error", err)) 76 | .connect(); 77 | value = JSON.parse((await client.hGet(key, field)) as string); 78 | client.destroy(); 79 | } else { 80 | value = list[key][field] || undefined; 81 | } 82 | return value as string; 83 | }; 84 | 85 | export const verifyId = async ( 86 | key: string, 87 | field: string, 88 | ): Promise => { 89 | return (await getData(key, field)) ? true : false; 90 | }; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elumian", 3 | "version": "1.0.0", 4 | "description": "Elumian es un framework modular diseñado específicamente para desarrolladores backend que necesitan desplegar APIs REST de forma rápida, eficiente y segura. Proporciona una estructura clara y herramientas integradas para la creación de módulos, gestión de servicios, validación de datos y manejo de excepciones HTTP, facilitando el desarrollo escalable y mantenible.", 5 | "main": "lib/*", 6 | "homepage": "https://github.com/KrashMello/elumian", 7 | "type": "commonjs", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/KrashMello/elumian" 11 | }, 12 | "keywords": [ 13 | "elumian", 14 | "elumian-ts", 15 | "lib", 16 | "rest", 17 | "api rest" 18 | ], 19 | "files": [ 20 | "lib/" 21 | ], 22 | "author": "krashMello (https://krashmello.vercel.app/)", 23 | "license": "MIT", 24 | "scripts": { 25 | "tsc": "tsc", 26 | "build": "tsc -b -v src && rm -f lib/**/*.tsbuildinfo", 27 | "prebuild:prod": "tsc -b --clean src", 28 | "build:watch": "tsc -b -v src --watch", 29 | "start": "node dist/src/index.js", 30 | "start:example": "ts-node-dev --rs --watch --clear -r tsconfig-paths/register example/index.ts", 31 | "migrate": "ts-node example/migration/index.ts", 32 | "dev": "ts-node -r example/index.ts", 33 | "test": "vitest", 34 | "lint": "ts-standard", 35 | "lint:fix": "ts-standard --fix", 36 | "db:generate": "prisma generate --schema=./example/prisma/scheme.prisma", 37 | "db:push": "prisma db push --schema=./example/prisma/scheme.prisma", 38 | "db:seed": "ts-node ./example/prisma/seed.ts" 39 | }, 40 | "devDependencies": { 41 | "@biomejs/biome": "2.1.3", 42 | "@types/cors": "^2.8.19", 43 | "@types/twig": "^1.12.17", 44 | "module-alias": "^2.2.3", 45 | "ts-node": "^10.9.2", 46 | "ts-node-dev": "^2.0.0", 47 | "ts-standard": "^12.0.2", 48 | "tsc-alias": "^1.8.16", 49 | "tsconfig-paths": "4.2.0", 50 | "vitest": "^3.2.4" 51 | }, 52 | "dependencies": { 53 | "@prisma/client": "^6.12.0", 54 | "@types/bcrypt": "^6.0.0", 55 | "@types/express": "5.0.3", 56 | "@types/lodash": "^4.17.20", 57 | "@types/node": "^24.0.13", 58 | "@types/validator": "13.15.2", 59 | "bcrypt": "^6.0.0", 60 | "cors": "2.8.5", 61 | "dotenv": "^17.2.0", 62 | "express": "5.1.0", 63 | "lodash": "^4.17.21", 64 | "prisma": "^6.12.0", 65 | "redis": "^5.6.1", 66 | "reflect-metadata": "^0.2.2", 67 | "socket.io": "^4.8.1", 68 | "tslib": "^2.8.1", 69 | "twig": "^1.17.1", 70 | "typescript": "*", 71 | "validator": "13.15.15" 72 | }, 73 | "eslintConfig": { 74 | "parserOptions": { 75 | "project": "./tsconfig.json" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/core/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | const SECRET_KEY: string = process.env.eln_SECRET_KEY || "secretKey"; 3 | 4 | const ALGORITHM: string = "aes-256-cbc"; 5 | const SECRET_KEY_BUFFER: crypto.CipherKey = Buffer.alloc( 6 | 32, 7 | SECRET_KEY, 8 | "utf8", 9 | ); 10 | const IV: Buffer = crypto.randomBytes(16); 11 | const TIMER_ENCODE = { 12 | 1: "cgtzG1lTxwn8Ha", 13 | 2: "jS1ycnzt6DFVPK", 14 | 3: "GUcAT5SGpG5CPj", 15 | 4: "jyqs88DO3iSyjo", 16 | }; 17 | /* 18 | * Generates a random string of characters. 19 | * @param max - The maximum length of the string. 20 | * @returns The generated string. 21 | */ 22 | export const codeGen = (max: number = 32): string => { 23 | const chars = 24 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*/_"; 25 | let result = "km-"; 26 | const charLength = chars.length; 27 | for (let i = 0; i < max - result.length; i++) { 28 | if (i % 15 === 0) result += "/"; 29 | else { 30 | const randomIndex = Math.floor(Math.random() * charLength); 31 | result += chars.charAt(randomIndex); 32 | } 33 | } 34 | return result; 35 | }; 36 | 37 | export const encrypted = (data: object, unsafe: boolean = false): string => { 38 | const plainText: string = JSON.stringify(data); 39 | let iv = IV; 40 | if (unsafe) iv = Buffer.alloc(16, SECRET_KEY, "utf8"); 41 | const cipher: crypto.Cipheriv = crypto.createCipheriv( 42 | ALGORITHM, 43 | SECRET_KEY_BUFFER, 44 | iv, 45 | ); 46 | return cipher.update(plainText, "utf8", "hex") + cipher.final("hex"); 47 | }; 48 | 49 | export const encryptedBase64 = ( 50 | text: string, 51 | unsafe: boolean = false, 52 | ): string => { 53 | let iv = IV; 54 | if (unsafe) iv = Buffer.alloc(16, SECRET_KEY, "utf8"); 55 | const cipher: crypto.Cipheriv = crypto.createCipheriv( 56 | ALGORITHM, 57 | SECRET_KEY_BUFFER, 58 | iv, 59 | ); 60 | const encrypted = Buffer.concat([ 61 | cipher.update(text), 62 | cipher.final(), 63 | ]).toString("base64"); 64 | 65 | return encrypted; 66 | }; 67 | 68 | export const decrypt = ( 69 | data: string, 70 | unsafe: boolean = false, 71 | ): Record => { 72 | let iv = IV; 73 | if (unsafe) iv = Buffer.alloc(16, SECRET_KEY, "utf8"); 74 | const decipher: crypto.Decipheriv = crypto.createDecipheriv( 75 | ALGORITHM, 76 | SECRET_KEY_BUFFER, 77 | iv, 78 | ); 79 | const decryptedText: string = 80 | decipher.update(data, "hex", "utf8") + decipher.final("utf8"); 81 | return JSON.parse(decryptedText); 82 | }; 83 | 84 | export const decryptBase64 = ( 85 | text: string, 86 | unsafe: boolean = false, 87 | ): string => { 88 | let iv = IV; 89 | if (unsafe) iv = Buffer.alloc(16, SECRET_KEY, "utf8"); 90 | const encryptedText = Buffer.from(text, "base64"); 91 | const decipher: crypto.Decipheriv = crypto.createDecipheriv( 92 | ALGORITHM, 93 | SECRET_KEY_BUFFER, 94 | iv, 95 | ); 96 | return Buffer.concat([ 97 | decipher.update(encryptedText), 98 | decipher.final(), 99 | ]).toString(); 100 | }; 101 | 102 | export const hardEncrypt = (data: object, unsafe: boolean = false): string => { 103 | const time = Math.floor(Math.random() * 4) + 1; 104 | 105 | let encryptText = encrypted(data, unsafe); 106 | for (let i = 0; i < time; i++) { 107 | encryptText = encryptedBase64(encryptText, unsafe); 108 | } 109 | return `${TIMER_ENCODE[time]}.${encryptText}`; 110 | }; 111 | 112 | export const hardDecrypt = ( 113 | data: string, 114 | unsafe: boolean = false, 115 | ): Record => { 116 | const [Stime, encryptText] = data.split("."); 117 | const time = Number( 118 | Object.entries(TIMER_ENCODE).find((v) => v[1] === Stime)[0], 119 | ); 120 | let decryptText = encryptText; 121 | for (let i = 0; i < time; i++) { 122 | decryptText = decryptBase64(decryptText, unsafe); 123 | } 124 | return decrypt(decryptText, unsafe); 125 | }; 126 | -------------------------------------------------------------------------------- /src/common/request/validations.ts: -------------------------------------------------------------------------------- 1 | import { type IsAlphaOptions, type Locale } from "./type"; 2 | 3 | const alpha = { 4 | "es-ES": /^[a-zA-Z-.\s&,_#!*/]+$/i, 5 | }; 6 | const alphanumeric = { 7 | "es-ES": /^[a-z0-9A-Z-.\s&,_#/]+$/i, 8 | }; 9 | 10 | const FORMATS = [ 11 | "YYYY-MM-DD", 12 | "YYYY/MM/DD", 13 | "DD-MM-YYYY", 14 | "DD/MM/YYYY", 15 | "MM-DD-YYYY", 16 | "MM/DD/YYYY", 17 | ]; 18 | 19 | export function isDate(dateString: string, format: string): boolean { 20 | const formatIndex = FORMATS.indexOf(format); 21 | if (formatIndex === -1) { 22 | throw new Error(`Invalid date format: ${format}`); 23 | } 24 | 25 | const parts = dateString.split(/[-/]/); 26 | if (parts.length !== 3) { 27 | return false; 28 | } 29 | 30 | const year = parseInt( 31 | parts[formatIndex === 0 || formatIndex === 1 ? 0 : 2], 32 | 10, 33 | ); 34 | const month = parseInt( 35 | parts[formatIndex === 0 || formatIndex === 3 ? 1 : 0], 36 | 10, 37 | ); 38 | const day = parseInt( 39 | parts[formatIndex === 0 || formatIndex === 1 ? 2 : 1], 40 | 10, 41 | ); 42 | 43 | if (isNaN(year) || isNaN(month) || isNaN(day)) { 44 | return false; 45 | } 46 | 47 | const date = new Date(year, month - 1, day); 48 | return ( 49 | date.getFullYear() === year && 50 | date.getMonth() === month - 1 && 51 | date.getDate() === day 52 | ); 53 | } 54 | 55 | export function isAlphaSimbols( 56 | _str: string, 57 | locale: Locale = "es-ES", 58 | options: IsAlphaOptions = { ignore: "" }, 59 | ): any { 60 | let str = _str; 61 | const ignore = options.ignore; 62 | 63 | if (ignore instanceof RegExp || typeof ignore === "string") { 64 | if (ignore instanceof RegExp) { 65 | str = str.replace(ignore, ""); 66 | } else if (typeof ignore === "string") { 67 | str = str.replace( 68 | new RegExp( 69 | "[".concat(ignore.replace(/[[\]{}()*+?.,\\^$|#]/g, "\\$&"), "]"), 70 | "g", 71 | ), 72 | "", 73 | ); // escape regex for ignore 74 | } else { 75 | throw new Error("ignore should be instance of a String or RegExp"); 76 | } 77 | } 78 | if (locale in alpha) { 79 | return alpha["es-ES"].test(str); 80 | } 81 | 82 | throw new Error("Invalid locale '".concat(locale, "'")); 83 | } 84 | export function isAlphaNumericSimbols( 85 | _str: string, 86 | locale: Locale = "es-ES", 87 | options: any = {}, 88 | ): any { 89 | let str = _str; 90 | const ignore = options.ignore; 91 | 92 | if (ignore instanceof RegExp || typeof ignore === "string") { 93 | if (ignore instanceof RegExp) { 94 | str = str.replace(ignore, ""); 95 | } else if (typeof ignore === "string") { 96 | str = str.replace( 97 | new RegExp( 98 | "[".concat(ignore.replace(/[[\]{}()*+?.,\\^$|#]/g, "\\$&"), "]"), 99 | "g", 100 | ), 101 | "", 102 | ); // escape regex for ignore 103 | } else { 104 | throw new Error("ignore should be instance of a String or RegExp"); 105 | } 106 | } 107 | 108 | if (locale in alphanumeric) { 109 | return alphanumeric["es-ES"].test(str); 110 | } 111 | 112 | throw new Error("Invalid locale '".concat(locale, "'")); 113 | } 114 | 115 | export function isAlpha( 116 | _str: string, 117 | locale: Locale = "es-ES", 118 | options: IsAlphaOptions = {}, 119 | ): any { 120 | let str = _str; 121 | const { ignore } = options; 122 | 123 | if (ignore instanceof RegExp || typeof ignore === "string") { 124 | if (ignore instanceof RegExp) { 125 | str = str.replace(ignore, ""); 126 | } else if (typeof ignore === "string") { 127 | str = str.replace( 128 | new RegExp( 129 | `[${ignore.replace(/[-[\]{}()*+?.,\\^$|#\\s]/g, "\\$&")}]`, 130 | "g", 131 | ), 132 | "", 133 | ); // escape regex for ignore 134 | } else { 135 | throw new Error("ignore should be instance of a String or RegExp"); 136 | } 137 | } 138 | 139 | if (locale in alpha) { 140 | return alpha["es-ES"].test(str); 141 | } 142 | throw new Error(`Invalid locale '${locale}'`); 143 | } 144 | -------------------------------------------------------------------------------- /src/common/request/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAlpha, 3 | isAlphaSimbols, 4 | isDate, 5 | isAlphaNumericSimbols, 6 | } from "./validations"; 7 | import { isAlphanumeric, isNumeric } from "validator"; 8 | import isEmail from "validator/lib/isEmail"; 9 | import isBoolean from "validator/lib/isBoolean"; 10 | import { 11 | validationsMessage, 12 | validationsOptions, 13 | validationsOptionsFields, 14 | } from "./type"; 15 | export let messages = { 16 | min: "Min characters length must be ", 17 | max: "Max characters length must be ", 18 | alpha: "Characters must be a-zA-Z", 19 | alphaSimbol: "Characters must be a-zA-Z -.&,_#!*/", 20 | alphaNumeric: "Characters must be a-zA-Z1-9", 21 | numeric: "Please enter a valid number (e.g., 1234)", 22 | alphaNumericSimbols: "Characters must be a-zA-Z1-9 -.&,_#*/", 23 | email: "Please enter a valid email address (e.g., foo@gmail.com)", 24 | boolean: "Please enter a boolean", 25 | date: "Please enter a valid date (e.g., 2020-01-01)", 26 | required: `The must be required`, 27 | invalidField: "field is not valid", 28 | }; 29 | export const setMessages = (newMessages: validationsMessage) => { 30 | messages = { ...messages, ...newMessages }; 31 | }; 32 | 33 | export class validations { 34 | static dataCompare = { 35 | min: (value: string, length: number) => value && value.length >= length, 36 | max: (value: string, length: number) => value && value.length <= length, 37 | alpha: (value: string) => (value ? isAlpha(value, "es-ES") : true), 38 | alphaSimbol: (value: string) => 39 | value ? isAlphaSimbols(value ?? "", "es-ES") : true, 40 | alphaNumeric: (value: string) => 41 | value ? isAlphanumeric(value, "es-ES", { ignore: " " }) : true, 42 | numeric: (value: string) => (value ? isNumeric(value) : true), 43 | alphaNumericSimbols: (value: string) => 44 | value ? isAlphaNumericSimbols(value, "es-ES") : true, 45 | email: (value: string) => (value ? isEmail(value) : true), 46 | boolean: (value: any) => (value ? isBoolean(value) : true), 47 | date: (value: string) => (value ? isDate(value, "YYYY-MM-DD") : true), 48 | required: (value: string) => !!value, 49 | }; 50 | static validateField( 51 | optionsToValidate: validationsOptionsFields[], 52 | key: string, 53 | data: Record, 54 | ): string[] { 55 | return optionsToValidate 56 | .map((v: validationsOptionsFields) => { 57 | if (v.includes("min") || v.includes("max")) { 58 | const [type, limit]: ["min" | "max", number] = v.split(":") as [ 59 | "min" | "max", 60 | number, 61 | ]; 62 | return this.dataCompare[type](data[key], Number(limit)) 63 | ? null 64 | : messages[type] + Number(limit); 65 | } 66 | if (!this.dataCompare[v]) { 67 | return messages.invalidField; 68 | } 69 | 70 | if (v === "required") messages[v] = messages[v].replace("", key); 71 | 72 | return this.dataCompare[v](data[key]) ? null : messages[v]; 73 | }) 74 | .filter((z: null | string) => z); 75 | } 76 | static compareData( 77 | data: Record, 78 | optionsToValidate: validationsOptions, 79 | ): true | Record { 80 | if (typeof data !== "object" || data === null) 81 | throw new Error("parameter data must be an object"); 82 | if (typeof optionsToValidate !== "object" || optionsToValidate === null) 83 | throw new Error("parameter optionsToValidate must be an object"); 84 | 85 | const keysData = Object.keys(data); 86 | const keysOptionsV = Object.keys(optionsToValidate); 87 | let errors: Record; 88 | if (keysData.filter((x) => !keysOptionsV.includes(x)).length > 0) { 89 | const m = messages.invalidField; 90 | errors = { 91 | ...errors, 92 | ...Object.fromEntries( 93 | keysData 94 | .filter((x) => !keysOptionsV.includes(x)) 95 | .map((key) => { 96 | return [key, m]; 97 | }), 98 | ), 99 | }; 100 | } 101 | const optionsEntries = Object.entries(optionsToValidate); 102 | errors = { 103 | ...errors, 104 | ...Object.fromEntries( 105 | optionsEntries 106 | .map(([key]: any) => { 107 | let errorsValidate: (string | false)[] | string = []; 108 | if (optionsToValidate[key]) 109 | errorsValidate = this.validateField( 110 | optionsToValidate[key], 111 | key, 112 | data, 113 | ); 114 | return errorsValidate.length > 0 ? [key, errorsValidate[0]] : null; 115 | }) 116 | .filter((k) => k), 117 | ), 118 | }; 119 | return Object.keys(errors).length > 0 ? errors : true; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Documentación detallada de Elumian 2 | 3 | ## Descripción general 4 | 5 | **Elumian** es un framework modular diseñado específicamente para desarrolladores backend que necesitan desplegar APIs REST de forma rápida, eficiente y segura. Proporciona una estructura clara y herramientas integradas para la creación de módulos, gestión de servicios, validación de datos y manejo de excepciones HTTP, facilitando el desarrollo escalable y mantenible. 6 | 7 | ## Instalación 8 | 9 | Puedes instalar Elumian en tu proyecto utilizando cualquiera de los gestores de paquetes más populares de JavaScript: 10 | 11 | - Con **npm**: 12 | 13 | ```sh 14 | npm i Elumian 15 | ``` 16 | 17 | - Con **yarn**: 18 | 19 | ```sh 20 | yarn add Elumian 21 | ``` 22 | 23 | - Con **pnpm**: 24 | 25 | ```sh 26 | pnpm add Elumian 27 | ``` 28 | 29 | ## Uso con TypeScript 30 | 31 | Elumian está pensado para integrarse fácilmente con TypeScript, ofreciendo tipados claros y anotaciones que permiten un desarrollo más seguro y robusto. 32 | 33 | ### Definición de Módulos 34 | 35 | Elumian utiliza decoradores para definir módulos que agrupan controladores, servicios y middlewares. 36 | 37 | ```typescript 38 | import { Module } from "elumian/common" 39 | 40 | @Module({ 41 | controllers: [TestController], 42 | services: [TestService], 43 | middlewares: [GlobalMiddleware] 44 | }) 45 | class TestModule { } 46 | ``` 47 | 48 | - `controllers`: Lista de controladores que gestionan las rutas y peticiones. 49 | - `services`: Servicios que manejan la lógica de negocio. 50 | - `middlewares`: Middlewares globales para manejo transversal de peticiones. 51 | 52 | ### Definición de Servicios 53 | 54 | Los servicios encapsulan la lógica reutilizable. Se definen también mediante decoradores. 55 | 56 | ```typescript 57 | import { Service } from "elumian/common" 58 | 59 | @Service 60 | class TestService { 61 | data = { 62 | nombre: "asdf" 63 | } 64 | 65 | getData() { 66 | return { 67 | status: HttpStatus.ok, 68 | message: this.data, 69 | type: 'SUCCESS' 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | - El método `getData()` retorna un objeto con un estado HTTP, datos y un tipo de respuesta. 76 | 77 | ### Creación de Controladores y Rutas 78 | 79 | Los controladores usan decoradores para definir rutas HTTP y validaciones sobre diferentes partes de la solicitud. 80 | 81 | ```typescript 82 | import { ValidateQuery, ValidateParams, ValidateBody, Post, Get, Controller } from "elumian/common" 83 | import { HttpExceptions, HttpStatus } from "elumian/common" 84 | import { bodyDataValidate } from "./dataValidateSchemas/bodyData.schema" 85 | 86 | @Controller("test") 87 | class TestController { 88 | @Get("/") 89 | getData(req, res) { 90 | HttpExceptions(Elumian.TestService.getData()) 91 | } 92 | 93 | @Post("/test/1") 94 | @ValidateBody(bodyDataValidate) 95 | @ValidateQuery(bodyDataValidate) 96 | test1(req, res) { 97 | HttpExceptions({ 98 | status: HttpStatus.ok, 99 | message: { test: "test" }, 100 | type: 'SUCCESS' 101 | }) 102 | } 103 | 104 | @Post("/:id") 105 | @ValidateParams(bodyDataValidate) 106 | @ValidateBody(bodyDataValidate) 107 | test2(req, res) { 108 | HttpExceptions({ 109 | status: HttpStatus.ok, 110 | message: { test: "test" }, 111 | type: 'SUCCESS' 112 | }) 113 | } 114 | } 115 | ``` 116 | 117 | - `@Controller("test")`: Prefijo global para las rutas del controlador. 118 | - `@Get()`, `@Post()`: Definición de métodos HTTP. 119 | - `@ValidateBody`, `@ValidateQuery`, `@ValidateParams`: Validaciones aplicadas a diferentes partes de la solicitud basadas en esquemas. 120 | - `HttpExceptions`: Maneja respuestas estandarizadas y excepciones HTTP. 121 | 122 | ### Definición de Esquemas de Validación 123 | 124 | Los esquemas de validación definen reglas para los datos recibidos en las peticiones, utilizando las opciones de validación propias de Elumian. 125 | 126 | ```typescript 127 | import { validationsOptions } from "elumian/type" 128 | 129 | export const bodyDataValidate: validationsOptions = { 130 | fecha: ['required', 'date'], 131 | } 132 | ``` 133 | 134 | - En este ejemplo, el campo `fecha` es obligatorio y debe cumplir con el formato fecha. 135 | 136 | ## Inicialización y Arranque del Servidor 137 | 138 | Para iniciar tu servidor Elumian, configura los módulos que quieres cargar y define el puerto de escucha. Posteriormente, inicia el servidor. 139 | 140 | ```typescript 141 | import { Server } from "elumian/core"; 142 | 143 | Server.setConfig({ modules: [modulo1, modulo2], port: 5000 }) 144 | Server.start() 145 | ``` 146 | 147 | - `modules`: Array con los módulos que contiene la aplicación. 148 | - `port`: Puerto en el que el servidor escuchará las peticiones entrantes. 149 | -------------------------------------------------------------------------------- /src/common/request/type.d.ts: -------------------------------------------------------------------------------- 1 | import { AlphaLocale } from "validator"; 2 | import { validations } from "." 3 | export interface Message { 4 | alpha?: string; 5 | alphaSimbols?: string; 6 | alphaNumeric?: string; 7 | alphaNumericSimbols?: string; 8 | numeric?: string; 9 | required?: string; 10 | boolean?: string; 11 | min?: string; 12 | max?: string; 13 | array?: string; 14 | email?: string; 15 | } 16 | 17 | type BaseValidations = keyof typeof validations.dataCompare; 18 | type MinMaxValidations = `min:${number}` | `max:${number}`; 19 | type validationsOptionsFields = BaseValidations | MinMaxValidations; 20 | 21 | export type validationsOptions = Record< 22 | string, 23 | Array 24 | >; 25 | 26 | export type validationsMessage = Partial< 27 | Record 28 | >; 29 | 30 | export type dataCompareValueRequest = Record; 31 | 32 | export type returnCompareValue = true | string | Record; 33 | 34 | export interface IsAlphaSimbolsOptions { 35 | ignore?: string; 36 | } 37 | 38 | export type Locale = 39 | | "en-US" 40 | | "bg-BG" 41 | | "cs-CZ" 42 | | "da-DK" 43 | | "de-DE" 44 | | "el-GR" 45 | | "es-AR" 46 | | "es-ES" 47 | | "fr-FR" 48 | | "it-IT" 49 | | "nb-NO" 50 | | "nl-NL" 51 | | "nn-NO" 52 | | "hu-HU" 53 | | "pl-PL" 54 | | "pt-PT" 55 | | "ru-RU" 56 | | "sl-SI" 57 | | "sk-SK" 58 | | "sr-RS@latin" 59 | | "sr-RS" 60 | | "sv-SE" 61 | | "tr-TR" 62 | | "uk-UA" 63 | | "ku-IQ" 64 | | "ar" 65 | | "he" 66 | | "fa-IR" 67 | | "en-AU" 68 | | "en-GB" 69 | | "en-HK" 70 | | "en-IN" 71 | | "en-NZ" 72 | | "en-ZA" 73 | | "en-ZM" 74 | | "ar-AE" 75 | | "ar-BH" 76 | | "ar-DZ" 77 | | "ar-EG" 78 | | "ar-IQ" 79 | | "ar-JO" 80 | | "ar-KW" 81 | | "ar-LB" 82 | | "ar-LY" 83 | | "ar-MA" 84 | | "ar-QM" 85 | | "ar-QA" 86 | | "ar-SA" 87 | | "ar-SD" 88 | | "ar-SY" 89 | | "ar-TN" 90 | | "ar-YE" 91 | | "pt-BR" 92 | | "pl-Pl"; 93 | 94 | export interface IsAlphaOptions { 95 | /** 96 | * @default undefined 97 | */ 98 | ignore?: string | RegExp | undefined; 99 | } 100 | 101 | export interface IsLengthOptions { 102 | /** 103 | * @default 0 104 | */ 105 | min?: number | undefined; 106 | /** 107 | * @default undefined 108 | */ 109 | max?: number | undefined; 110 | } 111 | 112 | export interface IsNumericOptions { 113 | /** 114 | * If `no_symbols` is true, the validator will reject numeric strings that feature a symbol (e.g. `+`, `-`, or `.`). 115 | * 116 | * @default false 117 | */ 118 | no_symbols?: boolean | undefined; 119 | locale?: AlphaLocale | undefined; 120 | } 121 | 122 | export interface IsEmailOptions { 123 | /** 124 | * If `allow_display_name` is set to `true`, the validator will also match `Display Name `. 125 | * 126 | * @default false 127 | */ 128 | allow_display_name?: boolean | undefined; 129 | /** 130 | * If `require_display_name` is set to `true`, the validator will reject strings without the format `Display Name `. 131 | * 132 | * @default false 133 | */ 134 | require_display_name?: boolean | undefined; 135 | /** 136 | * If `allow_utf8_local_part` is set to `false`, the validator will not allow any non-English UTF8 character in email address' local part. 137 | * 138 | * @default true 139 | */ 140 | allow_utf8_local_part?: boolean | undefined; 141 | /** 142 | * If `require_tld` is set to `false`, e-mail addresses without having TLD in their domain will also be matched. 143 | * 144 | * @default true 145 | */ 146 | require_tld?: boolean | undefined; 147 | /** 148 | * If `ignore_max_length` is set to `true`, the validator will not check for the standard max length of an email. 149 | * 150 | * @default false 151 | */ 152 | ignore_max_length?: boolean | undefined; 153 | /** 154 | * If `allow_ip_domain` is set to `true`, the validator will allow IP addresses in the host part. 155 | * 156 | * @default false 157 | */ 158 | allow_ip_domain?: boolean | undefined; 159 | /** 160 | * If `domain_specific_validation` is `true`, some additional validation will be enabled, 161 | * e.g. disallowing certain syntactically valid email addresses that are rejected by GMail. 162 | * 163 | * @default false 164 | */ 165 | domain_specific_validation?: boolean | undefined; 166 | /** 167 | * If host_blacklist is set to an array of strings 168 | * and the part of the email after the @ symbol matches one of the strings defined in it, 169 | * the validation fails. 170 | */ 171 | host_blacklist?: string[] | undefined; 172 | /** 173 | * If blacklisted_chars receives a string, then the validator will reject emails that include 174 | * any of the characters in the string, in the name part. 175 | */ 176 | blacklisted_chars?: string | undefined; 177 | } 178 | 179 | export interface isBooleansOptions { 180 | /** 181 | * If loose is is set to false, the validator will strictly match ['true', 'false', '0', '1']. 182 | * If loose is set to true, the validator will also match 'yes', 'no', and will match a valid boolean string of any case. (eg: ['true', 'True', 'TRUE']). 183 | * @default false 184 | */ 185 | loose?: boolean | undefined; 186 | } 187 | 188 | export interface ResponseValidate { 189 | status: number; 190 | message?: string; 191 | } 192 | -------------------------------------------------------------------------------- /src/core/server/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import type { Express } from "express"; 3 | import * as os from "os"; 4 | import * as http from "http"; 5 | // import { Server as ServerIO, Socket } from "socket.io"; 6 | import * as cors from "cors"; 7 | import { type CorsOptions } from "cors"; 8 | import { Elumian } from "../elumian"; 9 | import { ServerConfig } from "./type"; 10 | import type { SRouter, IRouter, moduleMetadata } from "elumian/common/type"; 11 | import { RouteInfo, type SocketInfo, SocketRoute } from "./type"; 12 | import type { NextFunction, Request, Response } from "express"; 13 | import { setMessages, validations } from "elumian/common"; 14 | 15 | const socketInfo: SocketInfo[] = []; 16 | const socketRoutes: SocketRoute[] = []; 17 | 18 | function registerSocketRoutes( 19 | routesSocket: SRouter[], 20 | prefix: string, 21 | instance: any, 22 | controllerName: string, 23 | ) { 24 | routesSocket.forEach(({ method, pathName, handlerName }) => { 25 | const socketPath = `${prefix}:${pathName}`; 26 | socketRoutes.push({ 27 | method, 28 | path: socketPath, 29 | functionSocket: (io: Server, socket: any) => { 30 | instance[handlerName](io, socket); 31 | }, 32 | }); 33 | socketInfo.push({ 34 | method, 35 | path: socketPath, 36 | handlerName: `${controllerName}.${handlerName}`, 37 | }); 38 | }); 39 | } 40 | 41 | function getIPV4(): string { 42 | const { networkInterfaces } = os; 43 | const nets = networkInterfaces(); 44 | let IPV4: string = "127.0.0.1"; 45 | 46 | for (const name of Object.keys(nets)) { 47 | const auxNets: (os.NetworkInterfaceInfo | null)[] | undefined = nets[name]; 48 | if (auxNets) { 49 | for (const net of auxNets) { 50 | const familyV4Value: string | number = 51 | typeof net.family === "string" ? "IPv4" : 4; 52 | if ( 53 | net.family === familyV4Value && 54 | !net.internal && 55 | typeof net.address === "string" 56 | ) { 57 | IPV4 = net.address; 58 | break; 59 | } 60 | } 61 | } 62 | } 63 | 64 | return IPV4; 65 | } 66 | 67 | export class Server { 68 | private static app: Express = express(); 69 | private static config: ServerConfig; 70 | private static routesInfo: RouteInfo[] = []; 71 | static setRedisProvider(args: { url: string }) { 72 | Elumian.cache.setConfigProvider(args); 73 | } 74 | static setConfig = (config: ServerConfig) => { 75 | this.app.use(express.json()); 76 | this.config = config; 77 | }; 78 | static chargeModules = (modules: any[]) => { 79 | for (let modul of modules) { 80 | const controllers = Reflect.getMetadata("controllers", modul); 81 | let middlewares = Reflect.getMetadata("middlewares", modul); 82 | const services = Reflect.getMetadata("services", modul); 83 | //NOTE: aqui empiezo a cargar los servicios de los modulos 84 | if (services && services.length > 0) 85 | for (let service of services) { 86 | const initialService = service.getInstance(); 87 | const prefix = Reflect.getMetadata("prefix", service); 88 | if (!Elumian[prefix]) Elumian[prefix] = initialService; 89 | } 90 | //NOTE: aqui empiezo a cargar los controladores de los modulos 91 | for (let controller of controllers) { 92 | const prefix = Reflect.getMetadata("prefix", controller); 93 | const controllerHandlerName = Reflect.getMetadata( 94 | "handlerName", 95 | controller, 96 | ); 97 | //NOTE: aqui se hace una auto inject dependencies 98 | const dependencies = 99 | Reflect.getMetadata("design:paramtypes", controller) || []; 100 | const controllerPrototype = controller.prototype; 101 | const instances = dependencies.map((dep) => dep.getInstance()); 102 | controller = new controller(...instances); 103 | let descriptors = Object.getOwnPropertyDescriptors(controllerPrototype); 104 | let routes = express.Router(); 105 | //NOTE: en esta fase empieza a leer todas las funciones incluido el constructor 106 | for (const propertyKey in descriptors) { 107 | let guards = []; 108 | const descriptor = descriptors[propertyKey]; 109 | //NOTE: chequeo que no sea el constructor 110 | if ( 111 | propertyKey !== "constructor" && 112 | typeof descriptor.value == "function" 113 | ) { 114 | this.routesInfo.push({ 115 | controller: controllerHandlerName + "@" + propertyKey, 116 | method: ( 117 | Reflect.getMetadata("method", descriptor.value) || "get" 118 | ).toUpperCase(), 119 | path: 120 | `http://localhost:${this.config.port}/${prefix}${Reflect.getMetadata("path", descriptor.value)}` || 121 | `http://localhost:${this.config.port}/${prefix}/`, 122 | }); 123 | const method = 124 | Reflect.getMetadata("method", descriptor.value) || "get"; 125 | const path = Reflect.getMetadata("path", descriptor.value) || "/"; 126 | //NOTE:: aqui cargamos en el router las funciones de los controladores 127 | routes[method]( 128 | path, 129 | ...middlewares.map((middleware) => { 130 | let init = Reflect.getMetadata("middleware", middleware); 131 | init = Object.getOwnPropertyDescriptors(init).value.value; 132 | return async (req, res, next) => { 133 | try { 134 | const context = { 135 | handler: controller[propertyKey], 136 | request: req, 137 | response: res, 138 | }; 139 | if (await init(context)) next(); 140 | } catch (e) { 141 | if (e.data) res.status(e.status).json(e.data); 142 | else { 143 | console.log(e); 144 | res.status(500).json({ 145 | message: "Internal server error", 146 | type: "DANGER", 147 | }); 148 | } 149 | } 150 | }; 151 | }), 152 | async (req, res, next) => { 153 | try { 154 | await controller[propertyKey](req, res, next); 155 | } catch (e) { 156 | if (e.data) res.status(e.status).json(e.data); 157 | else { 158 | console.log(e); 159 | res.status(500).json({ 160 | message: "Internal server error", 161 | type: "DANGER", 162 | }); 163 | } 164 | } 165 | }, 166 | ); 167 | } 168 | } 169 | this.app.use(`/${prefix}`, routes); 170 | } 171 | } 172 | }; 173 | static configCors = (args: { whiteList: string[] | string }) => { 174 | const { whiteList } = args; 175 | const corsOptions: CorsOptions = 176 | typeof whiteList === "object" 177 | ? { 178 | origin: (origin, callback) => { 179 | if (!origin || (whiteList && whiteList.includes(origin))) { 180 | callback(null, true); 181 | } else { 182 | callback(new Error("Not allowed by CORS")); 183 | } 184 | }, 185 | } 186 | : { 187 | origin: whiteList, 188 | }; 189 | this.app.use(cors(corsOptions)); 190 | }; 191 | static start = (): void => { 192 | if (Object.keys(this.config).length === 0) { 193 | console.log("the config need been set"); 194 | return; 195 | } 196 | this.config.port = this.config.port ?? 5000; 197 | const httpServer: http.Server = http.createServer(this.app); 198 | // const io: Server = new Server(httpServer); 199 | this.app.use((_req: Request, res: Response) => { 200 | res.status(404).json({ message: "Ruta no encontrada!" }); 201 | }); 202 | console.table(this.routesInfo); 203 | httpServer.listen(this.config.port, () => { 204 | const IPV4: string = getIPV4(); 205 | console.log( 206 | `Network: http://${IPV4}:${this.config.port} \nlocal: http://localhost:${this.config.port}`, 207 | ); 208 | }); 209 | }; 210 | } 211 | --------------------------------------------------------------------------------