├── .eslintignore ├── common ├── shields │ └── spring │ │ └── jwt-auth.shield.ts ├── types │ ├── dolph_port.type.ts │ ├── media_types.type.ts │ ├── dolph_env.type.ts │ ├── validators.types.ts │ ├── dolph_middleware.type.ts │ ├── mongoose_types.type.ts │ ├── auth.type.ts │ ├── response.type.ts │ ├── events.type.ts │ ├── index.ts │ ├── hash_param.type.ts │ ├── spring.types.ts │ ├── dolph.types.ts │ └── dolph_uploader.type.ts ├── constants │ ├── index.ts │ ├── messages.constant.ts │ └── error_constants.types.ts ├── api │ ├── HttpStatus.api.ts │ ├── index.ts │ ├── ErrorException.api.ts │ ├── exceptions │ │ ├── gone.api.ts │ │ ├── conflict.api.ts │ │ ├── forbidden.api.ts │ │ ├── no_content.api.ts │ │ ├── not_found.api.ts │ │ ├── bad_gateway.api.ts │ │ ├── bad_request.api.ts │ │ ├── im_a_teapot.api.ts │ │ ├── not_modified.api.ts │ │ ├── timeout.api.ts │ │ ├── unauthorized.api.ts │ │ ├── not_acceptable.api.ts │ │ ├── misdirected.api.ts │ │ ├── not_implemented.api.ts │ │ ├── payment_required.api.ts │ │ ├── method_not_allowed.api.ts │ │ ├── service_unavailable.api.ts │ │ ├── internal_server_error.ts │ │ ├── unsupported_media_type.api.ts │ │ ├── http_version_not_suported.api.ts │ │ ├── default_exception.api.ts │ │ └── index.ts │ ├── pick.api.ts │ ├── SuccessRespone.api.ts │ └── ErrorResponse.api.ts ├── index.ts ├── interfaces │ ├── index.ts │ ├── mongoose.interface.ts │ ├── jwt.interfaces.ts │ ├── media_parser.interfaces.ts │ ├── socket.interfaces.ts │ └── dolph.interfaces.ts └── middlewares │ ├── index.ts │ ├── jwt_auth.middleware.ts │ ├── default_middleware.middleware.ts │ ├── validate_request_middleware.middleware.ts │ ├── try_catch_middleware.middleware.ts │ └── global_validation_middleware.middlewares.ts ├── packages ├── spring │ └── spring_module.packages.ts ├── memdb │ ├── index.ts │ └── memdb.packages.ts ├── sequelize │ ├── index.ts │ └── sequelize_connection.package.ts ├── sockets │ ├── socket_component.packages.ts │ ├── index.ts │ └── socket_io.packages.ts ├── mongoose │ ├── index.ts │ ├── mongoose_connection.package.ts │ └── transform_doc_plugin.package.ts ├── index.ts ├── uploader │ ├── counter.ts │ ├── errors │ │ └── error_messages.ts │ ├── remove_uploaded_files.ts │ ├── index.ts │ ├── file_appender.ts │ ├── file_uploader.ts │ └── make_middleware.ts └── events │ └── events_module.packages.ts ├── core ├── initializers │ ├── service_registeries.core.ts │ ├── index.ts │ ├── middleware_registrar.ts │ └── dependency_injection.core.ts ├── adapters │ ├── index.ts │ ├── exception_handlers.ts │ └── mvc_registrar.ts ├── index.ts ├── config.core.ts ├── fallback_middleware.core.ts ├── morgan.core.ts ├── error.core.ts └── transformer.ts ├── .lintstagedrc.json ├── decorators ├── events │ ├── index.ts │ └── event.decorator.ts ├── dolph │ ├── index.ts │ └── dolph_service.decorator.ts ├── sockets │ ├── index.ts │ └── sockets.decorators.ts ├── mysql │ ├── index.ts │ └── mysql_initializer.decorators.ts ├── validations │ ├── index.ts │ └── validations.decorators.ts ├── mongoose │ ├── index.ts │ └── mongoose_service_intializer.decorators.ts ├── others │ ├── index.ts │ ├── try_catch_async.decorator.ts │ └── cookie_auth_verify.decorators.ts ├── spring │ ├── meta_data_keys.decorators.ts │ └── index.ts └── index.ts ├── utilities ├── media │ ├── index.ts │ └── file_extensions.utilities.ts ├── encryptions │ ├── index.ts │ └── bcrypt_encryption.utilities.ts ├── auth │ ├── index.ts │ ├── JWT_generator.utilities.ts │ └── cookie.utilities.ts ├── validators │ ├── index.ts │ ├── objectId_validator.validator.ts │ └── password_validator.validator.ts ├── index.ts ├── normalize_path.utilities.ts ├── get_controllers_from_component.ts ├── is_component.utilities.ts ├── formatters │ └── format_date.utilities.ts ├── spring_helpers.utilities.ts ├── generate_unique_codes.utilities.ts └── logger.utilities.ts ├── samples ├── demo │ ├── dolph_config.yaml │ ├── .gitignore │ ├── dolph_cli.yaml │ ├── src │ │ ├── components │ │ │ └── user │ │ │ │ ├── user.model.ts │ │ │ │ ├── user2.service.ts │ │ │ │ ├── user.component.ts │ │ │ │ ├── user.dto.ts │ │ │ │ ├── user.service.ts │ │ │ │ ├── user.shield.ts │ │ │ │ ├── user2.controller.ts │ │ │ │ └── user.controller.ts │ │ ├── resolvers │ │ │ ├── user.entity.ts │ │ │ └── user.resolver.ts │ │ └── server.ts │ ├── .swcrc │ ├── package.json │ └── tsconfig.json ├── basic │ ├── index.routes.ts │ ├── sqlDb.ts │ ├── app.auth_function.ts │ ├── views │ │ └── layouts │ │ │ └── main.handlebars │ ├── app.middleware.ts │ ├── event_emitter.service.ts │ ├── app.component.ts │ ├── new.dto.ts │ ├── test_event_service_dep.service.ts │ ├── app.schema.ts │ ├── socket.component.ts │ ├── app.validator.ts │ ├── app.model.ts │ ├── another_event.service.ts │ ├── new.service.ts │ ├── event.service.ts │ ├── app.server.ts │ ├── app.service.ts │ ├── app.router.ts │ ├── new.controller.ts │ └── app.controller.ts └── tsconfig.json ├── views ├── about.handlebars ├── home.handlebars ├── about.ejs ├── home.ejs ├── partials │ ├── header.pug │ └── header.ejs ├── about.pug └── home.pug ├── .gitattributes ├── tools ├── gulp │ ├── gulpfile.ts │ ├── config.tools.ts │ ├── tsconfig.json │ ├── tasks │ │ ├── copy.tool.ts │ │ └── clean.tool.ts │ └── utilities │ │ └── helpers.task.ts └── benchmarks │ └── basic.ts ├── .prettierignore ├── README.md ├── .prettierrc ├── tsconfig.spec.json ├── jest.setup.js ├── classes ├── index.ts ├── service_classes.class.ts ├── router_classes.class.ts ├── event_service_classes.class.ts ├── controller_classes.class.ts └── jwt_auth_classes.class.ts ├── .editorconfig ├── .gitignore ├── renovate.json ├── index.ts ├── compile.sh ├── .npmignore ├── dolph_config.yaml ├── gulpfile.js ├── jest.config.js ├── tests ├── core.test.ts ├── middleware.test.ts ├── uploader │ ├── memorystorage.test.ts │ ├── diskstorage.test.ts │ └── fileuploader.test.ts ├── fallback_middleware.test.ts └── try_catch.test.ts ├── tsconfig.json ├── todo.txt ├── LICENSE ├── .eslintrc.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | -------------------------------------------------------------------------------- /common/shields/spring/jwt-auth.shield.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/spring/spring_module.packages.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/initializers/service_registeries.core.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": "eslint" 3 | } 4 | -------------------------------------------------------------------------------- /core/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mvc_registrar'; 2 | -------------------------------------------------------------------------------- /packages/memdb/index.ts: -------------------------------------------------------------------------------- 1 | export * from './memdb.packages'; 2 | -------------------------------------------------------------------------------- /decorators/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event.decorator'; 2 | -------------------------------------------------------------------------------- /utilities/media/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './parser.uilities'; 2 | -------------------------------------------------------------------------------- /decorators/dolph/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dolph_service.decorator'; 2 | -------------------------------------------------------------------------------- /decorators/sockets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sockets.decorators'; 2 | -------------------------------------------------------------------------------- /common/types/dolph_port.type.ts: -------------------------------------------------------------------------------- 1 | export type dolphPort = number | string; 2 | -------------------------------------------------------------------------------- /decorators/mysql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mysql_initializer.decorators'; 2 | -------------------------------------------------------------------------------- /decorators/validations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validations.decorators'; 2 | -------------------------------------------------------------------------------- /samples/demo/dolph_config.yaml: -------------------------------------------------------------------------------- 1 | port: 3000 2 | globalExceptionFilter: true -------------------------------------------------------------------------------- /common/types/media_types.type.ts: -------------------------------------------------------------------------------- 1 | export type mediaType = 'single' | 'array'; 2 | -------------------------------------------------------------------------------- /packages/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sequelize_connection.package'; 2 | -------------------------------------------------------------------------------- /packages/sockets/socket_component.packages.ts: -------------------------------------------------------------------------------- 1 | export class SocketComponent {} 2 | -------------------------------------------------------------------------------- /utilities/encryptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bcrypt_encryption.utilities'; 2 | -------------------------------------------------------------------------------- /views/about.handlebars: -------------------------------------------------------------------------------- 1 |

About Page

2 |

Learn more about us here.

3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yarn.lock -diff -merge 2 | yarn.lock linguist-generated=true 3 | -------------------------------------------------------------------------------- /views/home.handlebars: -------------------------------------------------------------------------------- 1 |

Home Page

2 |

Welcome to the {{ title}} page!

3 | -------------------------------------------------------------------------------- /decorators/mongoose/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongoose_service_intializer.decorators'; 2 | -------------------------------------------------------------------------------- /samples/demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | logs 4 | yarn-error.log 5 | app 6 | uploads/ -------------------------------------------------------------------------------- /tools/gulp/gulpfile.ts: -------------------------------------------------------------------------------- 1 | // import './tasks/clean.tool'; 2 | import './tasks/copy.tool'; 3 | -------------------------------------------------------------------------------- /common/types/dolph_env.type.ts: -------------------------------------------------------------------------------- 1 | export type dolphEnv = 'development' | 'test' | 'production'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/**/*.d.ts 2 | packages/**/*.js 3 | .nyc_output 4 | node_modules/ 5 | coverage/ -------------------------------------------------------------------------------- /common/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error_constants.types'; 2 | export * from './messages.constant'; 3 | -------------------------------------------------------------------------------- /utilities/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JWT_generator.utilities'; 2 | export * from './cookie.utilities'; 3 | -------------------------------------------------------------------------------- /packages/sockets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './socket_io.packages'; 2 | export * from './socket_component.packages'; 3 | -------------------------------------------------------------------------------- /views/about.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header') %> 2 |

About Page

3 |

Learn more about us here.

4 | -------------------------------------------------------------------------------- /core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dolph_factory.core'; 2 | export * from './initializers'; 3 | export * from './adapters'; 4 | -------------------------------------------------------------------------------- /core/initializers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dependency_injection.core'; 2 | export * from './middleware_registrar'; 3 | -------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header') %> 2 |

<%=title %> Page

3 |

Welcome to the home page!

4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DolphJS Framework 2 | 3 | DolphJs runs on both Node and Bun. 4 | 5 | See [documentation](https://dolphjs.com) 6 | -------------------------------------------------------------------------------- /decorators/others/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cookie_auth_verify.decorators'; 2 | export * from './try_catch_async.decorator'; 3 | -------------------------------------------------------------------------------- /packages/mongoose/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transform_doc_plugin.package'; 2 | export * from './mongoose_connection.package'; 3 | -------------------------------------------------------------------------------- /utilities/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password_validator.validator'; 2 | export * from './objectId_validator.validator'; 3 | -------------------------------------------------------------------------------- /views/partials/header.pug: -------------------------------------------------------------------------------- 1 | header 2 | h1 My MVC App With Pug 3 | nav 4 | a(href="/") Home 5 | a(href="/about") About 6 | -------------------------------------------------------------------------------- /common/api/HttpStatus.api.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | 3 | const HttpStatus = httpStatus; 4 | export { HttpStatus }; 5 | -------------------------------------------------------------------------------- /common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './middlewares'; 3 | export * from './types'; 4 | export * from './interfaces'; 5 | -------------------------------------------------------------------------------- /common/types/validators.types.ts: -------------------------------------------------------------------------------- 1 | type passwordStrength = 'basic' | 'medium' | 'strong' | 'extraStrong'; 2 | export { passwordStrength }; 3 | -------------------------------------------------------------------------------- /samples/basic/index.routes.ts: -------------------------------------------------------------------------------- 1 | import { AppRouter } from './app.router'; 2 | 3 | const routes = [new AppRouter()]; 4 | 5 | export { routes }; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 125, 4 | "trailingComma": "all", 5 | "tabWidth": 4, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /decorators/spring/meta_data_keys.decorators.ts: -------------------------------------------------------------------------------- 1 | export const SHIELD_METADATA_KEY = 'shield'; 2 | export const UN_SHIELD_METADATA_KEY = 'unshield'; 3 | -------------------------------------------------------------------------------- /common/types/dolph_middleware.type.ts: -------------------------------------------------------------------------------- 1 | import { DRequestHandler } from '../interfaces'; 2 | 3 | export type DolphMiddlewareFn = (middlewares: DRequestHandler[]) => void; 4 | -------------------------------------------------------------------------------- /samples/basic/sqlDb.ts: -------------------------------------------------------------------------------- 1 | import { initMySql } from '../../packages'; 2 | 3 | const mysql = initMySql('dolph', 'root', 'password', 'localhost'); 4 | 5 | export { mysql }; 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["integration/**/*", "**/*.spec.ts"], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongoose'; 2 | export * from './sequelize'; 3 | export * from './memdb'; 4 | export * from './sockets'; 5 | export * from './uploader'; 6 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 |
2 |

My MVC App With EJS

3 | 7 |
8 | -------------------------------------------------------------------------------- /common/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.interfaces'; 2 | export * from './mongoose.interface'; 3 | export * from './dolph.interfaces'; 4 | export * from './socket.interfaces'; 5 | -------------------------------------------------------------------------------- /views/about.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title About 5 | body 6 | include partials/header 7 | h1 About Page 8 | p Learn more about us here. 9 | -------------------------------------------------------------------------------- /views/home.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Home 5 | body 6 | include partials/header 7 | h1 #{title} Page 8 | p Welcome to the home page! 9 | -------------------------------------------------------------------------------- /samples/demo/dolph_cli.yaml: -------------------------------------------------------------------------------- 1 | # this is an auto-generated file, please do not edit manually 2 | language: ts 3 | paradigm: oop 4 | database: mongo 5 | generateFolder: true 6 | routing: spring 7 | -------------------------------------------------------------------------------- /common/interfaces/mongoose.interface.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | interface MongooseConfig { 4 | url: string; 5 | options?: mongoose.ConnectOptions; 6 | } 7 | 8 | export { MongooseConfig }; 9 | -------------------------------------------------------------------------------- /decorators/spring/index.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container, Inject, InjectMany } from 'typedi'; 2 | 3 | export { Service, Container, Inject, InjectMany }; 4 | 5 | export * from './spring_package_decorator.decorators'; 6 | -------------------------------------------------------------------------------- /common/types/mongoose_types.type.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | type mongooseUrl = string; 4 | type mongooseConnectionOptions = mongoose.ConnectOptions; 5 | 6 | export { mongooseUrl, mongooseConnectionOptions }; 7 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(10000); 2 | 3 | jest.mock('fs-temp', () => ({ 4 | mkdir: () => Promise.resolve('/tmp/test-dir'), 5 | })); 6 | 7 | jest.mock('rimraf', () => ({ 8 | rimraf: () => Promise.resolve(), 9 | })); 10 | -------------------------------------------------------------------------------- /utilities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validators'; 2 | // export * from './media'; 3 | export * from './logger.utilities'; 4 | export * from './encryptions'; 5 | export * from './auth'; 6 | export * from './generate_unique_codes.utilities'; 7 | -------------------------------------------------------------------------------- /classes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './service_classes.class'; 2 | export * from './controller_classes.class'; 3 | export * from './router_classes.class'; 4 | export * from './jwt_auth_classes.class'; 5 | export * from './event_service_classes.class'; 6 | -------------------------------------------------------------------------------- /common/types/auth.type.ts: -------------------------------------------------------------------------------- 1 | export type sub = string | object | Buffer; 2 | 3 | export type cookieContent = { 4 | name: string; 5 | value: string; 6 | expires: Date; 7 | httpOnly: boolean; 8 | secure: boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /common/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HttpStatus.api'; 2 | export * from './pick.api'; 3 | export * from './ErrorException.api'; 4 | export * from './ErrorResponse.api'; 5 | export * from './SuccessRespone.api'; 6 | export * from './exceptions/index'; 7 | -------------------------------------------------------------------------------- /common/types/response.type.ts: -------------------------------------------------------------------------------- 1 | import { DResponse } from '../interfaces'; 2 | 3 | type ResponseType = { 4 | res: DResponse; 5 | status?: number; 6 | msg?: string; 7 | body?: T; 8 | }; 9 | 10 | export { ResponseType }; 11 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Document, model } from 'mongoose'; 2 | 3 | export interface IUser extends Document {} 4 | 5 | const UserSchema = new Schema({}); 6 | 7 | export const UserModel = model('users', UserSchema); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | 10 | [{*.js,*.json,*.yml,*.ts}] 11 | indent_size = 4 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /samples/basic/app.auth_function.ts: -------------------------------------------------------------------------------- 1 | import { IPayload } from '../../common'; 2 | 3 | export const authFunc = (payload: IPayload) => { 4 | console.log(payload); 5 | 6 | if (payload.info) { 7 | return false; 8 | } 9 | return true; 10 | }; 11 | -------------------------------------------------------------------------------- /tools/gulp/config.tools.ts: -------------------------------------------------------------------------------- 1 | import { getDirs } from './utilities/helpers.task'; 2 | 3 | export const source = ['common', 'classes', 'core', 'decorators', 'packages', 'utilities']; 4 | export const samplePath = 'sample'; 5 | 6 | export const packagePath = getDirs(source); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | 4 | dolph-app 5 | 6 | dist 7 | 8 | # Logs 9 | logs 10 | *.log 11 | *.local 12 | 13 | # Misc 14 | .DS_Store 15 | .fleet 16 | .idea 17 | 18 | # Local env files 19 | .env 20 | .env.* 21 | !.env.example 22 | 23 | tests/uploads/ -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":dependencyDashboard"], 3 | "labels": ["dependencies"], 4 | "packageRules": [ 5 | { 6 | "matchDepTypes": ["devDependencies"], 7 | "automerge": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /common/interfaces/jwt.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IPayload { 2 | sub: string | object | Buffer; 3 | iat: number; 4 | exp: number; 5 | info?: any; 6 | } 7 | 8 | export interface AuthorizationFunction { 9 | (payload: IPayload): Promise | boolean; 10 | } 11 | -------------------------------------------------------------------------------- /utilities/normalize_path.utilities.ts: -------------------------------------------------------------------------------- 1 | export const normalizePath = (path: string) => { 2 | // Replace all backslashes with forward slashes for windows machines 3 | let normalized = path.replace(/\\/g, '/'); 4 | return normalized.startsWith('/') ? normalized : `/${normalized}`; 5 | }; 6 | -------------------------------------------------------------------------------- /samples/basic/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | {{{body}}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /common/types/events.type.ts: -------------------------------------------------------------------------------- 1 | export type Listener = { 2 | listener: (...args: T[]) => void | boolean; 3 | priority: number; 4 | once: boolean; 5 | }; 6 | 7 | export type ListenerDecType = { 8 | listener: Function; 9 | priority: number; 10 | once: boolean; 11 | }; 12 | -------------------------------------------------------------------------------- /samples/basic/app.middleware.ts: -------------------------------------------------------------------------------- 1 | import { DNextFunc, DRequest, DResponse } from '../../common'; 2 | 3 | export const testMiddleware = (req: DRequest, res: DResponse, next: DNextFunc) => { 4 | req.payload = { 5 | sub: '', 6 | exp: 0, 7 | iat: 0, 8 | }; 9 | next(); 10 | }; 11 | -------------------------------------------------------------------------------- /common/interfaces/media_parser.interfaces.ts: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import { mediaType } from '..'; 3 | 4 | export interface IMediaParserOptions { 5 | extensions?: string[]; 6 | type: mediaType; 7 | storage?: multer.DiskStorageOptions; 8 | fieldname: string; 9 | limit?: number; 10 | } 11 | -------------------------------------------------------------------------------- /common/api/ErrorException.api.ts: -------------------------------------------------------------------------------- 1 | import { DefaultException } from './exceptions/default_exception.api'; 2 | 3 | /** 4 | * throws a new error exception including the stack, error and message 5 | * similar to `DefaultException` 6 | * 7 | */ 8 | class ErrorException extends DefaultException {} 9 | export { ErrorException }; 10 | -------------------------------------------------------------------------------- /samples/basic/event_emitter.service.ts: -------------------------------------------------------------------------------- 1 | import { DService } from '../../decorators'; 2 | import { EventEmitterService } from '../../packages/events/events_module.packages'; 3 | 4 | @DService() 5 | export class TestEventEmitterService extends EventEmitterService {} 6 | 7 | export const testEventEmitterService = new TestEventEmitterService(); 8 | -------------------------------------------------------------------------------- /samples/basic/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../decorators'; 2 | import { AppService } from './app.service'; 3 | import { NewController } from './new.controller'; 4 | import { NewService } from './new.service'; 5 | 6 | @Component({ controllers: [NewController], services: [NewService, AppService] }) 7 | export class AppComponent {} 8 | -------------------------------------------------------------------------------- /samples/basic/new.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsNumber, IsString } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | name: string; 7 | 8 | @IsNumber() 9 | age: number; 10 | 11 | @IsString() 12 | @IsEmail() 13 | @IsNotEmpty() 14 | email: string; 15 | } 16 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as common from './common'; 2 | import * as classes from './classes'; 3 | import * as decorators from './decorators'; 4 | import * as packages from './packages'; 5 | import * as utilities from './utilities'; 6 | 7 | import 'reflect-metadata'; 8 | 9 | export { common, classes, decorators, packages, utilities }; 10 | export * from './core'; 11 | -------------------------------------------------------------------------------- /samples/basic/test_event_service_dep.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphServiceHandler } from '../../classes'; 2 | import { Dolph } from '../../common'; 3 | 4 | export class TestEventService extends DolphServiceHandler { 5 | constructor() { 6 | super('test'); 7 | } 8 | 9 | logData(data: any) { 10 | console.log(data); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/demo/src/resolvers/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Field, Int, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | class User { 5 | @Field((type) => Int) 6 | id: number; 7 | 8 | @Field((type) => String) 9 | name: string; 10 | 11 | @Field((type) => String, { nullable: true }) 12 | email?: string; 13 | } 14 | 15 | export { User }; 16 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user2.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphServiceHandler } from '../../../../../classes'; 2 | import { Dolph } from '../../../../../common'; 3 | 4 | export class User2Service extends DolphServiceHandler { 5 | constructor() { 6 | super('userService'); 7 | } 8 | 9 | get() { 10 | return 'Hello'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /common/api/exceptions/gone.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.GONE` exception 6 | */ 7 | class GoneException extends DefaultException { 8 | statusCode: number = HttpStatus.GONE; 9 | name: string = 'Gone'; 10 | } 11 | 12 | export { GoneException }; 13 | -------------------------------------------------------------------------------- /decorators/index.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container } from 'typedi'; 2 | 3 | export * from './mongoose'; 4 | export * from './dolph'; 5 | export * from './mysql'; 6 | export * from './others'; 7 | export * from './spring'; 8 | export * from './validations'; 9 | export * from './sockets'; 10 | export * from './events'; 11 | 12 | export { Service as DService, Container as DContainer }; 13 | -------------------------------------------------------------------------------- /utilities/validators/objectId_validator.validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * verify's if a string is of type `mongoose.SchemaTypes.ObjectID` 4 | */ 5 | const objectId = (value: any, helpers: any) => { 6 | if (!value.match(/^[0-9a-fA-F]{24}$/)) { 7 | return helpers.message('"{{#label}}" must be a valid mongo id'); 8 | } 9 | return value; 10 | }; 11 | 12 | export { objectId }; 13 | -------------------------------------------------------------------------------- /common/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './try_catch_middleware.middleware'; 2 | export * from './default_middleware.middleware'; 3 | export * from './validate_request_middleware.middleware'; 4 | export * from './jwt_auth.middleware'; 5 | export { 6 | validateBodyMiddleware, 7 | validateParamMiddleware, 8 | validateQueryMiddleware, 9 | } from './global_validation_middleware.middlewares'; 10 | -------------------------------------------------------------------------------- /common/api/exceptions/conflict.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.CONFLICT` exception 6 | */ 7 | class ConflictException extends DefaultException { 8 | statusCode: number = HttpStatus.CONFLICT; 9 | name: string = 'Conflict'; 10 | } 11 | 12 | export { ConflictException }; 13 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Specify the top-level directories where you want to start compilation 4 | directories=("classes" "common" "core" "decorators" "packages" "utilities") 5 | 6 | # Loop through the directories and run tsc for each one 7 | for dir in "${directories[@]}"; do 8 | echo "Compiling TypeScript in $dir" 9 | find "$dir" -type f -name '*.ts' -exec tsc -b -v {} + 10 | done 11 | 12 | -------------------------------------------------------------------------------- /common/api/exceptions/forbidden.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.FORBIDDEN` exception 6 | */ 7 | class ForbiddenException extends DefaultException { 8 | statusCode: number = HttpStatus.FORBIDDEN; 9 | name: string = 'Forbidden'; 10 | } 11 | 12 | export { ForbiddenException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/no_content.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.NO_CONTENT` exception 6 | */ 7 | class NoContentException extends DefaultException { 8 | statusCode: number = HttpStatus.NO_CONTENT; 9 | name: string = 'No Content'; 10 | } 11 | 12 | export { NoContentException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/not_found.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.NOT_FOUND` exception 6 | */ 7 | 8 | class NotFoundException extends DefaultException { 9 | statusCode: number = HttpStatus.NOT_FOUND; 10 | name: string = 'Not Found'; 11 | } 12 | 13 | export { NotFoundException }; 14 | -------------------------------------------------------------------------------- /common/api/exceptions/bad_gateway.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.BAD_GATEWAY` exception 6 | */ 7 | class BadGatewayException extends DefaultException { 8 | statusCode: number = HttpStatus.BAD_GATEWAY; 9 | name: string = 'Bad Gateway'; 10 | } 11 | 12 | export { BadGatewayException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/bad_request.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.BAD_REQUEST` exception 6 | */ 7 | class BadRequestException extends DefaultException { 8 | statusCode: number = HttpStatus.BAD_REQUEST; 9 | name: string = 'Bad Request'; 10 | } 11 | 12 | export { BadRequestException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/im_a_teapot.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.IM_A_TEAPOT` exception 6 | */ 7 | 8 | class ImTeaPotException extends DefaultException { 9 | statusCode: number = HttpStatus.IM_A_TEAPOT; 10 | name: string = "I'm a Teapot"; 11 | } 12 | 13 | export { ImTeaPotException }; 14 | -------------------------------------------------------------------------------- /common/api/exceptions/not_modified.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.NOT_MODIFIED` exception 6 | */ 7 | class NotModifiedException extends DefaultException { 8 | statusCode: number = HttpStatus.NOT_MODIFIED; 9 | name: string = 'Not Modified'; 10 | } 11 | 12 | export { NotModifiedException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/timeout.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.GATEWAY_TIMEOUT` exception 6 | */ 7 | class TimeOutException extends DefaultException { 8 | statusCode: number = HttpStatus.GATEWAY_TIMEOUT; 9 | name: string = 'Gateway Time Out'; 10 | } 11 | 12 | export { TimeOutException }; 13 | -------------------------------------------------------------------------------- /common/api/exceptions/unauthorized.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.UNAUTHORIZED` exception 6 | */ 7 | class UnauthorizedException extends DefaultException { 8 | statusCode: number = HttpStatus.UNAUTHORIZED; 9 | name: string = 'Unauthorized'; 10 | } 11 | 12 | export { UnauthorizedException }; 13 | -------------------------------------------------------------------------------- /tools/benchmarks/basic.ts: -------------------------------------------------------------------------------- 1 | const autocannon = require('autocannon'); 2 | 3 | const runBasicBenchmark = async () => { 4 | const url = 'http://localhost:3030'; 5 | const connections = 100; 6 | const duration = 20; 7 | 8 | const results = await autocannon({ 9 | url, 10 | connections, 11 | duration, 12 | }); 13 | 14 | autocannon.printResult(results); 15 | }; 16 | 17 | runBasicBenchmark(); 18 | -------------------------------------------------------------------------------- /common/api/exceptions/not_acceptable.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.NOT_ACCEPTABLE` exception 6 | */ 7 | class NotAcceptableException extends DefaultException { 8 | statusCode: number = HttpStatus.NOT_ACCEPTABLE; 9 | name: string = 'Not Acceptable'; 10 | } 11 | 12 | export { NotAcceptableException }; 13 | -------------------------------------------------------------------------------- /utilities/get_controllers_from_component.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { DolphControllerHandler } from '../classes'; 4 | import { Dolph } from '../common'; 5 | 6 | export function getControllersFromMetadata>(target: { 7 | new (): { controllers?: Array<{ new (): T }> }; 8 | }): Array<{ new (): T }> | undefined { 9 | return Reflect.getMetadata('controllers', target.prototype); 10 | } 11 | -------------------------------------------------------------------------------- /common/api/exceptions/misdirected.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.MISDIRECTED_REQUEST` exception 6 | */ 7 | 8 | class MisDirectedException extends DefaultException { 9 | statusCode: number = HttpStatus.MISDIRECTED_REQUEST; 10 | name: string = 'Misdirected Request'; 11 | } 12 | 13 | export { MisDirectedException }; 14 | -------------------------------------------------------------------------------- /common/api/exceptions/not_implemented.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.NOT_IMPLEMENTED` exception 6 | */ 7 | 8 | class NotImplementedException extends DefaultException { 9 | statusCode: number = HttpStatus.NOT_IMPLEMENTED; 10 | name: string = 'Not Implemented'; 11 | } 12 | 13 | export { NotImplementedException }; 14 | -------------------------------------------------------------------------------- /common/api/exceptions/payment_required.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.PAYMENT_REQUIRED` exception 6 | */ 7 | class PaymentRequiredException extends DefaultException { 8 | statusCode: number = HttpStatus.PAYMENT_REQUIRED; 9 | name: string = 'Payment Required'; 10 | } 11 | 12 | export { PaymentRequiredException }; 13 | -------------------------------------------------------------------------------- /core/config.core.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | config({}); 4 | 5 | export const configs = { 6 | NODE_ENV: process.env.NODE_ENV, 7 | PORT: +process.env.PORT, 8 | MONGO_URL: process.env.MONGO_URL, 9 | }; 10 | 11 | export const configLoader = () => { 12 | configs.NODE_ENV = process.env.NODE_ENV || 'development'; 13 | configs.PORT = +process.env.PORT || 3300; 14 | configs.MONGO_URL = process.env.MONGO_URL; 15 | }; 16 | -------------------------------------------------------------------------------- /samples/basic/app.schema.ts: -------------------------------------------------------------------------------- 1 | import { INTEGER, STRING } from 'sequelize'; 2 | import { mysql } from './sqlDb'; 3 | 4 | const User = mysql.define('user', { 5 | id: { 6 | type: INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | username: { 12 | type: STRING, 13 | allowNull: false, 14 | }, 15 | age: INTEGER, 16 | }); 17 | 18 | export { User }; 19 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../../../../../decorators'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | import { User2Controller } from './user2.controller'; 5 | import { User2Service } from './user2.service'; 6 | 7 | @Component({ controllers: [UserController, User2Controller], services: [UserService, User2Service] }) 8 | export class UserComponent {} 9 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; 3 | 4 | export class CreateUserDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | name: string; 8 | 9 | @IsNumber() 10 | age: number; 11 | 12 | @Type(() => String) 13 | @IsOptional() 14 | gender: string; 15 | 16 | @IsString() 17 | email: string; 18 | } 19 | -------------------------------------------------------------------------------- /common/api/exceptions/method_not_allowed.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.METHOD_NOT_ALLOWED` exception 6 | */ 7 | 8 | class MethodNotAllowedException extends DefaultException { 9 | statusCode: number = HttpStatus.METHOD_NOT_ALLOWED; 10 | name: string = 'Method Not Allowed'; 11 | } 12 | 13 | export { MethodNotAllowedException }; 14 | -------------------------------------------------------------------------------- /utilities/media/file_extensions.utilities.ts: -------------------------------------------------------------------------------- 1 | export const defaultFileExtensions = [ 2 | '.jpeg', 3 | '.png', 4 | '.jpg', 5 | '.xlsx', 6 | '.mp3', 7 | '.mp4', 8 | '.doc', 9 | '.docx', 10 | '.pdf', 11 | '.txt', 12 | '.webm', 13 | '.wmv', 14 | '.mpeg', 15 | '.mkv', 16 | '.mov', 17 | '.flv', 18 | '.html', 19 | '.xml', 20 | '.xhtml', 21 | '.avi', 22 | '.wav', 23 | '.bmi', 24 | ]; 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | 3 | log 4 | 5 | # configuration 6 | yarn-error.log 7 | tslint.json 8 | tsconfig.json 9 | tsconfig.spec.json 10 | .prettierrc 11 | 12 | *.tsbuildinfo 13 | 14 | views 15 | samples 16 | 17 | README.md 18 | renovate.json 19 | todo.txt 20 | LICENSE 21 | jset.setup.js 22 | jest.config.js 23 | gulpfile.js 24 | CONTRIBUTING.md 25 | compile.sh 26 | .prettierrignore 27 | .gitignore 28 | .gitattributes 29 | .editorconfig 30 | .eslintrc.js 31 | .eslintignore -------------------------------------------------------------------------------- /common/api/exceptions/service_unavailable.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.SERVICE_UNAVAILABLE` exception 6 | */ 7 | class ServiceUnavaliableException extends DefaultException { 8 | statusCode: number = HttpStatus.SERVICE_UNAVAILABLE; 9 | name: string = 'Service Unavailable'; 10 | } 11 | 12 | export { ServiceUnavaliableException }; 13 | -------------------------------------------------------------------------------- /dolph_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | # mongo: 3 | # url: sensitive 4 | # options: 5 | # mysql: 6 | # host: localhost 7 | # database: dolph 8 | # user: root 9 | # pass: 10 | middlewares: 11 | cors: 12 | activate: true 13 | origin: '' 14 | methods: 15 | - GET 16 | - POST 17 | - PUT 18 | - DELETE 19 | allowedHeaders: 20 | routing: 21 | base: '/v1' 22 | port: 3030 23 | # jsonLimit: 20mb 24 | # env: production 25 | -------------------------------------------------------------------------------- /common/api/exceptions/internal_server_error.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.INTERNAL_SERVER_ERROR` exception 6 | */ 7 | class InternalServerErrorException extends DefaultException { 8 | statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR; 9 | name: string = 'Internal Server Error'; 10 | } 11 | 12 | export { InternalServerErrorException }; 13 | -------------------------------------------------------------------------------- /samples/basic/socket.component.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from '../../decorators'; 2 | import { SocketComponent } from '../../packages'; 3 | import { AnotherEventService } from './another_event.service'; 4 | import { EventService } from './event.service'; 5 | import { TestEventService } from './test_event_service_dep.service'; 6 | 7 | @Socket({ services: [TestEventService], socketServices: [EventService, AnotherEventService] }) 8 | export class EventsComponent extends SocketComponent {} 9 | -------------------------------------------------------------------------------- /common/api/exceptions/unsupported_media_type.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.UNSUPPORTED_MEDIA_TYPE` exception 6 | */ 7 | 8 | class UnSupportedMediaException extends DefaultException { 9 | statusCode: number = HttpStatus.UNSUPPORTED_MEDIA_TYPE; 10 | name: string = 'Unsupported Media Type'; 11 | } 12 | 13 | export { UnSupportedMediaException }; 14 | -------------------------------------------------------------------------------- /common/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './response.type'; 2 | export * from './validators.types'; 3 | export * from './dolph_env.type'; 4 | export * from './dolph_port.type'; 5 | export * from './dolph_middleware.type'; 6 | export * from './hash_param.type'; 7 | export * from './mongoose_types.type'; 8 | export * from './media_types.type'; 9 | export * from './dolph.types'; 10 | export * from './auth.type'; 11 | export * from './spring.types'; 12 | export * from './events.type'; 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Load the TypeScript compiler, then load the TypeScript gulpfile which simply loads all 5 | * the tasks. The tasks are really inside tools/gulp/tasks. 6 | */ 7 | 8 | const path = require('path'); 9 | 10 | const projectDir = __dirname; 11 | const tsConfigPath = path.join(projectDir, 'tools/gulp/tsconfig.json'); 12 | 13 | require('ts-node').register({ 14 | project: tsConfigPath, 15 | }); 16 | 17 | require('./tools/gulp/gulpfile'); 18 | -------------------------------------------------------------------------------- /samples/basic/app.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | const createUser = { 4 | param: Joi.object().keys({ 5 | name: Joi.string().required(), 6 | age: Joi.number().required().min(15), 7 | work: Joi.string().required(), 8 | height: Joi.number().required(), 9 | }), 10 | }; 11 | 12 | const testCase = { 13 | body: Joi.object().keys({ 14 | name: Joi.string().required(), 15 | }), 16 | }; 17 | 18 | export { createUser, testCase }; 19 | -------------------------------------------------------------------------------- /utilities/is_component.utilities.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler } from '../classes'; 2 | import { DolphComponent } from '../common'; 3 | 4 | export function isComponentClass(obj: any): obj is DolphComponent { 5 | return ( 6 | typeof obj === 'object' && 7 | 'controllers' in obj && 8 | Array.isArray(obj.controllers) && 9 | obj.controllers.every((item: any) => typeof item === 'function' && item.prototype instanceof DolphControllerHandler) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /common/api/exceptions/http_version_not_suported.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../HttpStatus.api'; 2 | import { DefaultException } from './default_exception.api'; 3 | 4 | /** 5 | * @returns `HttpStatus.HTTP_VERSION_NOT_SUPPORTED` exception 6 | */ 7 | class HttpVersionUnSupportedException extends DefaultException { 8 | statusCode: number = HttpStatus.HTTP_VERSION_NOT_SUPPORTED; 9 | name: string = 'Http Version Not Supported'; 10 | } 11 | 12 | export { HttpVersionUnSupportedException }; 13 | -------------------------------------------------------------------------------- /samples/basic/app.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from 'mongoose'; 2 | 3 | export interface IUser extends Document { 4 | name: string; 5 | email: string; 6 | age: string; 7 | work: string; 8 | height: string; 9 | } 10 | 11 | const userSchema = new Schema({ 12 | name: String, 13 | email: String, 14 | age: Number, 15 | work: String, 16 | height: String, 17 | }); 18 | 19 | const userModel = mongoose.model('User', userSchema); 20 | export { userModel }; 21 | -------------------------------------------------------------------------------- /common/constants/messages.constant.ts: -------------------------------------------------------------------------------- 1 | import clc from 'cli-color'; 2 | 3 | export const dolphMessages = { 4 | coreUtilMessage: (title: string, content: string) => `${clc.bold(clc.green(`[${title}]:`))} ${clc.greenBright(content)}`, 5 | middlewareMessages: (component: string, componentName: string) => `Registered >>>> ${component} { ${componentName} }`, 6 | routeMessages: (methodName: string, req: string, method: string) => 7 | `Registered >>>> Route { ${methodName} at ${req.toUpperCase()} ${method} }`, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/sockets/socket_io.packages.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Server as SocketServer } from 'socket.io'; 3 | import { DSocket } from '../../common/interfaces/socket.interfaces'; 4 | 5 | @Service() 6 | export class SocketService { 7 | private socketIo: SocketServer; 8 | 9 | constructor(params: DSocket) { 10 | this.socketIo = new SocketServer(params.server, params.options); 11 | } 12 | 13 | public getSocket(): SocketServer { 14 | return this.socketIo; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /common/types/hash_param.type.ts: -------------------------------------------------------------------------------- 1 | type bcryptHashParam = { salt: number; pureString: string }; 2 | type bcryptCompareParam = { pureString: string; hashString: string }; 3 | type argonHashParam = { 4 | pureString: string; 5 | timeCost?: number; 6 | memoryCost?: number; 7 | parallelism?: number; 8 | type?: 0 | 1 | 2; 9 | version?: number; 10 | salt?: Buffer; 11 | saltLength?: number; 12 | raw?: true; 13 | secret?: Buffer; 14 | }; 15 | 16 | export { bcryptHashParam, bcryptCompareParam, argonHashParam }; 17 | -------------------------------------------------------------------------------- /utilities/formatters/format_date.utilities.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(timestamp: Date) { 2 | const date = new Date(timestamp); 3 | 4 | const year = date.getFullYear(); 5 | const month = String(date.getMonth() + 1).padStart(2, '0'); 6 | const day = String(date.getDate()).padStart(2, '0'); 7 | const hours = String(date.getHours()).padStart(2, '0'); 8 | const minutes = String(date.getMinutes()).padStart(2, '0'); 9 | const seconds = String(date.getSeconds()).padStart(2, '0'); 10 | 11 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 12 | } 13 | -------------------------------------------------------------------------------- /common/api/pick.api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an object composed of the picked object properties 3 | * - const filter = pick(req.query, ['limit', 'page']); would get the limit and page properties from the query object. 4 | * filter would become an object: 5 | * - { limit:any, page:any } 6 | */ 7 | const pick = (object: Object, keys: string[]) => { 8 | return keys.reduce((obj, key) => { 9 | if (object && Object.prototype.hasOwnProperty.call(object, key)) { 10 | obj[key] = object[key]; 11 | } 12 | return obj; 13 | }, {}); 14 | }; 15 | export { pick }; 16 | -------------------------------------------------------------------------------- /common/api/exceptions/default_exception.api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @extends Error 3 | */ 4 | class DefaultException extends Error { 5 | statusCode: number; 6 | isOperational: boolean; 7 | constructor(message: string, statusCode?: number, isOperational = true, stack = '') { 8 | super(message); 9 | this.statusCode = statusCode; 10 | this.isOperational = isOperational; 11 | if (stack) { 12 | this.stack = stack; 13 | } else { 14 | Error.captureStackTrace(this, this.constructor); 15 | } 16 | } 17 | } 18 | 19 | export { DefaultException }; 20 | -------------------------------------------------------------------------------- /common/interfaces/socket.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, Server, ServerResponse } from 'http'; 2 | import { ServerOptions } from 'socket.io'; 3 | import { Dolph, SocketServicesParams } from '../types'; 4 | import { SocketComponent, SocketService } from '../../packages'; 5 | 6 | export interface DSocket { 7 | server: Server; 8 | options?: Partial; 9 | } 10 | 11 | export interface DSocketInit { 12 | options?: Partial; 13 | component: SocketComponent; 14 | socketService?: typeof SocketService; 15 | } 16 | -------------------------------------------------------------------------------- /core/fallback_middleware.core.ts: -------------------------------------------------------------------------------- 1 | import httpStatus from 'http-status'; 2 | import { DNextFunc, DRequest, DResponse } from '../common'; 3 | 4 | export const fallbackResponseMiddleware = (req: DRequest, res: DResponse, next: DNextFunc) => { 5 | const originalSend = res.send; 6 | 7 | res.send = function (...args: any[]) { 8 | res.locals.responseSent = true; 9 | return originalSend.apply(res, args); 10 | }; 11 | 12 | next(); 13 | 14 | res.on('finish', () => { 15 | if (!res.locals.responseSent && !res.headersSent) { 16 | res.status(httpStatus.OK).send(); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig.json'); 3 | 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '/samples/', '/tools/'], 8 | roots: [''], 9 | transform: { 10 | '^.+\\.ts?$': 'ts-jest', 11 | }, 12 | transformIgnorePatterns: ['node_modules/(?!(fs-temp|random-path)/)'], 13 | setupFilesAfterEnv: ['/jest.setup.js'], 14 | // moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), 15 | }; 16 | -------------------------------------------------------------------------------- /tests/core.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { DolphFactory } from '../core'; 3 | 4 | describe('DolphJs Integration Test', () => { 5 | let server; 6 | 7 | beforeAll(() => { 8 | const app = new DolphFactory([]); 9 | server = app.start(); 10 | }); 11 | 12 | afterAll(() => { 13 | server.close(); 14 | }); 15 | 16 | it('should return 404 for unknown routes', async () => { 17 | const response = await request(server).get('/unknown-route'); 18 | expect(response.status).toBe(404); 19 | expect(response.body.message).toBe('this endpoint does not exist'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /samples/basic/another_event.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphSocketServiceHandler } from '../../classes'; 2 | import { Dolph } from '../../common'; 3 | 4 | export class AnotherEventService extends DolphSocketServiceHandler { 5 | constructor() { 6 | super(); 7 | this.socketService; 8 | this.handleEvents(); 9 | } 10 | 11 | private handleEvents() { 12 | this.socket.on('connection', (socket) => { 13 | socket.emit('connected', 'connection successful'); 14 | socket.on('new-user-two', (message) => { 15 | this.socket.emit('new-user-two', message); 16 | }); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tools/gulp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "noUnusedParameters": false, 5 | "noUnusedLocals": false, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "../../dist/tools/gulp", 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noImplicitThis": true, 12 | "noEmitOnError": true, 13 | "noImplicitAny": false, 14 | "target": "ES2021", 15 | "types": ["node"], 16 | "typeRoots": ["./typings", "../../node_modules/@types/"], 17 | "baseUrl": "." 18 | }, 19 | "files": ["gulpfile.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /decorators/others/try_catch_async.decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An asynchronous class-method decorator which wraps a method in a try-catch block 3 | * @version 1.4.3 4 | */ 5 | export const TryCatchAsyncDec = () => { 6 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 7 | const originalMethod = descriptor.value; 8 | 9 | descriptor.value = async function (...args: any[]) { 10 | const context = this; 11 | const [req, res, next] = args; 12 | try { 13 | await originalMethod.apply(context, args); 14 | } catch (err) { 15 | next(err); 16 | } 17 | }; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/uploader/counter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'stream'; 2 | 3 | export class Counter extends EventEmitter { 4 | private value: number = 0; 5 | 6 | increment(): void { 7 | this.value++; 8 | } 9 | 10 | decrement(): void { 11 | this.value--; 12 | if (this.value < 0) this.value = 0; 13 | if (this.value === 0) { 14 | this.emit('zero'); 15 | } 16 | } 17 | 18 | isZero(): boolean { 19 | return this.value === 0; 20 | } 21 | 22 | onceZero(fn: () => void): void { 23 | if (this.isZero()) { 24 | fn(); 25 | } else { 26 | this.once('zero', fn); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/demo/src/server.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'type-graphql'; 2 | import { DolphFactory } from '../../../core'; 3 | import { UserResolver } from './resolvers/user.resolver'; 4 | import { UserComponent } from './components/user/user.component'; 5 | // import { UserComponent } from './components/user/user.component'; 6 | 7 | const schema = async function createSchema() { 8 | return await buildSchema({ 9 | resolvers: [UserResolver], 10 | }); 11 | }; 12 | 13 | const context = async ({ req }) => ({ token: req.headers.token }); 14 | 15 | const dolph = new DolphFactory([UserComponent]); 16 | // const dolph = new DolphFactory({ graphql: true, schema: schema(), context }); 17 | dolph.start(); 18 | -------------------------------------------------------------------------------- /classes/service_classes.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types of dolph srvices: upperlevel and lowerlevel. 3 | * UpperLevel classes are used to interact with controllers while lowerlevel are used for interactions with one another 4 | */ 5 | 6 | import { Dolph } from '../common'; 7 | 8 | /** 9 | * Dolph's service handler 10 | * - takes a string generic 11 | * 12 | * - `name` accepts a unique name for the each service which is used behind the scenes by the controller handler 13 | * 14 | * @version 1.0.0 15 | */ 16 | abstract class DolphServiceHandler { 17 | public name: string; 18 | constructor(name: T) { 19 | this.name = name; 20 | } 21 | } 22 | 23 | export { DolphServiceHandler }; 24 | -------------------------------------------------------------------------------- /utilities/spring_helpers.utilities.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { SHIELD_METADATA_KEY, UN_SHIELD_METADATA_KEY } from '../decorators/spring/meta_data_keys.decorators'; 3 | import { Middleware } from '../common'; 4 | 5 | export const getShieldMiddlewares = (targetClass: any): Middleware[] | undefined => { 6 | return Reflect.getMetadata(SHIELD_METADATA_KEY, targetClass.prototype); 7 | }; 8 | 9 | export const getUnShieldMiddlewares = (targetMethod: any): Middleware[] | undefined => { 10 | return Reflect.getMetadata(UN_SHIELD_METADATA_KEY, targetMethod); 11 | }; 12 | 13 | export const getFunctionNames = (arr: Function[]) => { 14 | return arr.map((fn) => fn.name); 15 | }; 16 | 17 | export const stringifyFunction = (func: Function) => func.toString(); 18 | -------------------------------------------------------------------------------- /classes/router_classes.class.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DolphControllerHandler } from '.'; 3 | import { Dolph } from '../common'; 4 | /** 5 | * Dolph's route handler 6 | * - The `path` method represents the api endpoint to be used as path for all routes under this handler 7 | * 8 | * - The `initRoutes` method takes in routes to be used by this handler 9 | * ```js 10 | * initRoutes() { 11 | this.router.post(`${this.path}`, this.controller.sendGreeting); 12 | } 13 | * ``` 14 | * 15 | * @version 1.0.9 16 | */ 17 | abstract class DolphRouteHandler { 18 | abstract path: T; 19 | abstract initRoutes(): void; 20 | public router = Router(); 21 | abstract controller: DolphControllerHandler; 22 | } 23 | 24 | export { DolphRouteHandler }; 25 | -------------------------------------------------------------------------------- /samples/demo/src/resolvers/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'; 2 | import { User } from './user.entity'; 3 | 4 | @Resolver((of) => User) 5 | export class UserResolver { 6 | private users: User[] = []; 7 | 8 | @Query((returns) => [User]) 9 | async getUsers(@Ctx() context: any) { 10 | console.log(context.token); 11 | return this.users; 12 | } 13 | 14 | @Mutation((returns) => User) 15 | async addUser( 16 | @Arg('name', (type) => String) name: string, 17 | @Arg('email', (type) => String, { nullable: true }) email?: string, 18 | ): Promise { 19 | const user = { id: this.users.length + 1, name, email }; 20 | this.users.push(user); 21 | return user; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/gulp/tasks/copy.tool.ts: -------------------------------------------------------------------------------- 1 | import { dest, series, src, task } from 'gulp'; 2 | 3 | function copyPackageJson(): NodeJS.ReadWriteStream { 4 | return src('package.json').pipe(dest('dist')); 5 | } 6 | 7 | function copyReadme(): NodeJS.ReadWriteStream { 8 | return src('README.md').pipe(dest('dist')); 9 | } 10 | 11 | function copyLicense(): NodeJS.ReadWriteStream { 12 | return src('LICENSE').pipe(dest('dist')); 13 | } 14 | 15 | function copyNpmIgnore(): NodeJS.ReadWriteStream { 16 | return src('.npmignore').pipe(dest('dist')); 17 | } 18 | 19 | task('copy:package-json', copyPackageJson); 20 | task('copy:readme', copyReadme); 21 | task('copy:license', copyLicense); 22 | task('copy:npm-ignore', copyNpmIgnore); 23 | task('copy', series('copy:package-json', 'copy:readme', 'copy:license', 'copy:npm-ignore')); 24 | -------------------------------------------------------------------------------- /tools/gulp/tasks/clean.tool.ts: -------------------------------------------------------------------------------- 1 | import { task, src, series } from 'gulp'; 2 | import { source } from '../config.tools'; 3 | import * as clean from 'gulp-clean'; 4 | import * as deleteEmpty from 'delete-empty'; 5 | 6 | /** 7 | * Cleans the build output assets from the package folders 8 | */ 9 | 10 | function cleanOutput() { 11 | // `${source}/**/*.d.ts`; 12 | return src([`${source}/**/*.js`, `${source}/**/*.js.map`, `${source}/**/*.d.ts.map`], { 13 | read: false, 14 | }).pipe(clean()); 15 | } 16 | 17 | /** 18 | * Cleans empty directories 19 | */ 20 | 21 | function cleanDirs(done: () => void) { 22 | deleteEmpty.sync(`${source}/`); 23 | done(); 24 | } 25 | 26 | task('clean:output', cleanOutput); 27 | task('clean:dirs', cleanDirs); 28 | task('clean:bundle', series('clean:output', 'clean:dirs')); 29 | -------------------------------------------------------------------------------- /core/initializers/middleware_registrar.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | 3 | class MiddlewareRegistry { 4 | private static instance: MiddlewareRegistry; 5 | private middlewares: RequestHandler[] = []; 6 | 7 | private constructor() {} 8 | 9 | public static getInstance(): MiddlewareRegistry { 10 | if (!MiddlewareRegistry.instance) { 11 | MiddlewareRegistry.instance = new MiddlewareRegistry(); 12 | } 13 | 14 | return MiddlewareRegistry.instance; 15 | } 16 | 17 | public register(middleware: RequestHandler) { 18 | this.middlewares.push(middleware); 19 | } 20 | 21 | public getMiddlewares(): RequestHandler[] { 22 | return this.middlewares; 23 | } 24 | } 25 | 26 | export const middlewareRegistry = MiddlewareRegistry.getInstance(); 27 | -------------------------------------------------------------------------------- /common/api/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bad_gateway.api'; 2 | export * from './bad_request.api'; 3 | export * from './conflict.api'; 4 | export * from './forbidden.api'; 5 | export * from './gone.api'; 6 | export * from './http_version_not_suported.api'; 7 | export * from './im_a_teapot.api'; 8 | export * from './internal_server_error'; 9 | export * from './method_not_allowed.api'; 10 | export * from './misdirected.api'; 11 | export * from './no_content.api'; 12 | export * from './not_acceptable.api'; 13 | export * from './not_found.api'; 14 | export * from './not_implemented.api'; 15 | export * from './not_modified.api'; 16 | export * from './payment_required.api'; 17 | export * from './service_unavailable.api'; 18 | export * from './timeout.api'; 19 | export * from './unsupported_media_type.api'; 20 | export * from './unauthorized.api'; 21 | -------------------------------------------------------------------------------- /common/middlewares/jwt_auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { TryCatchAsyncFn } from '.'; 2 | import { JwtBasicAuth } from '../../classes'; 3 | import { DNextFunc, DRequest, DResponse } from '../interfaces'; 4 | 5 | /** 6 | * 7 | * Takes the `JwtBasicAuth` class as parameter and calls the Verify method to perform Authorization 8 | * 9 | * Passes the payload to the request body as `req.payload` 10 | * - see `IPayload` interface to see the design of the payload object 11 | */ 12 | function JwtAuthMiddleware(jwtBasicAuthInstance: JwtBasicAuth) { 13 | return TryCatchAsyncFn(async (req: DRequest, res: DResponse, next: DNextFunc) => { 14 | const payload = await jwtBasicAuthInstance.Verify(req, res, next); 15 | //@ts-expect-error 16 | req.payload = payload; 17 | next(); 18 | }); 19 | } 20 | 21 | export { JwtAuthMiddleware }; 22 | -------------------------------------------------------------------------------- /core/adapters/exception_handlers.ts: -------------------------------------------------------------------------------- 1 | // e.g., in common/exceptions/validation.exception.ts 2 | import { ValidationError } from 'class-validator'; 3 | // You might use a library like 'http-status-codes' for status codes 4 | const HTTP_STATUS_BAD_REQUEST = 400; 5 | 6 | export class ValidationException extends Error { 7 | public errors: ValidationError[]; 8 | public status: number; 9 | 10 | constructor(errors: ValidationError[], message: string = 'Input validation failed') { 11 | super(message); // Message for the error 12 | this.name = 'ValidationException'; 13 | this.errors = errors; // Array of validation errors from class-validator 14 | this.status = HTTP_STATUS_BAD_REQUEST; 15 | Object.setPrototypeOf(this, ValidationException.prototype); // Ensures 'instanceof ValidationException' works 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /decorators/mongoose/mongoose_service_intializer.decorators.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'mongoose'; 2 | 3 | /** 4 | * 5 | * this decorator is used to inject a mongoose model into the serviceHandler as a method 6 | * 7 | * @param propertyName takes the name of the mongoose model 8 | * @param model takes the actual mongoose model imported from the models dir 9 | * 10 | * 11 | * @version 2.0.0 12 | */ 13 | function InjectMongo(propertyName: string, model: Model) { 14 | return function (constructor: T) { 15 | const Wrapped = class extends constructor { 16 | [propertyName]: Model = model; 17 | }; 18 | Object.defineProperty(Wrapped, 'name', { value: constructor.name, configurable: true }); 19 | return Wrapped; 20 | }; 21 | } 22 | 23 | export { InjectMongo }; 24 | -------------------------------------------------------------------------------- /common/types/spring.types.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler, DolphServiceHandler, DolphSocketServiceHandler } from '../../classes'; 2 | import { DNextFunc, DRequest, DResponse } from '../../common/interfaces'; 3 | import { Dolph } from './dolph.types'; 4 | 5 | export type Middleware = (req: DRequest, res: DResponse, next: DNextFunc) => void; 6 | 7 | export type ComponentParams = { 8 | controllers: Array<{ new (): DolphControllerHandler }>; 9 | services?: Array<{ new (...args: any[]): {} }>; 10 | }; 11 | 12 | export type DolphComponent> = { 13 | controllers: Array<{ new (): T }>; 14 | }; 15 | 16 | export type SocketServicesParams = { 17 | services?: Array<{ new (): DolphServiceHandler }>; 18 | socketServices?: Array<{ new (): DolphSocketServiceHandler }>; 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "utilities/**/*", 4 | "common/**/*", 5 | "core/**/*", 6 | "decorators/**/*", 7 | "packages/**/*", 8 | "classes/**/*", 9 | "index.ts" 10 | ], 11 | "exclude": ["node_modules"], 12 | "compilerOptions": { 13 | "allowJs": false, 14 | "declaration": true, 15 | "experimentalDecorators": true, 16 | "sourceMap": true, 17 | "useUnknownInCatchVariables": false, 18 | "esModuleInterop": true, 19 | "strictFunctionTypes": false, 20 | "emitDeclarationOnly": false, 21 | "emitDecoratorMetadata": true, 22 | "declarationMap": false, 23 | "target": "ES2022", 24 | "rootDir": ".", 25 | "outDir": "dist", 26 | "pretty": true, 27 | "module": "commonjs" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphServiceHandler } from '../../../../../classes'; 2 | import { Dolph } from '../../../../../common'; 3 | import { InjectMongo } from '../../../../../decorators'; 4 | import { Model } from 'mongoose'; 5 | import { UserModel, IUser } from './user.model'; 6 | import { User2Service } from './user2.service'; 7 | 8 | @InjectMongo('userModel', UserModel) 9 | export class UserService extends DolphServiceHandler { 10 | userModel!: Model; 11 | users: { name: string; age: number; tag: string }[] = []; 12 | 13 | constructor(private User2Service: User2Service) { 14 | super('userservice'); 15 | } 16 | 17 | createUser(age: number, name: string): { name: string; age: number }[] { 18 | this.users.push({ age, name, tag: this.User2Service.get() }); 19 | return this.users; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/constants/error_constants.types.ts: -------------------------------------------------------------------------------- 1 | const DolphErrors = { 2 | serverError: 'internal server error', 3 | deprc: (value: string) => `${value} is depreciated`, 4 | notAllowed: (value: string) => `${value} is not allowed`, 5 | serverClosed: 'dolphjs server closed', 6 | sigtermReceived: 'SIGTREM received', 7 | passwordShort: (char: number | string) => `password must be at least ${char} characters`, 8 | passwordMustContain: (letter: number | string, no: number | string, symbols?: Array) => 9 | ` password must contain at least ${letter} letter and ${no} number ${ 10 | symbols.length ? `and any of these symbols:${symbols.join(', ')}` : '' 11 | }`, 12 | noDolphConfigFile: 13 | 'dolphjs engine cannot start without dolph_config.yaml file, please ensure to add it to root directory', 14 | }; 15 | 16 | export { DolphErrors }; 17 | -------------------------------------------------------------------------------- /packages/uploader/errors/error_messages.ts: -------------------------------------------------------------------------------- 1 | const errorMessages = { 2 | LIMIT_PART_COUNT: 'Too many parts', 3 | LIMIT_FILE_SIZE: 'File too large', 4 | LIMIT_FILE_COUNT: 'Too many files', 5 | LIMIT_FIELD_KEY: 'Field name too long', 6 | LIMIT_FIELD_VALUE: 'Field value too long', 7 | LIMIT_FIELD_COUNT: 'Too many fields', 8 | LIMIT_UNEXPECTED_FILE: 'Unexpected field', 9 | MISSING_FIELD_NAME: 'Field name missing', 10 | }; 11 | 12 | export class DolphFIleUploaderError extends Error { 13 | private code: string; 14 | private field: string; 15 | 16 | constructor(code: string, field: string) { 17 | super(errorMessages[code]); 18 | 19 | this.name = 'UploaderError'; 20 | 21 | this.code = code; 22 | 23 | if (field) { 24 | this.field = field; 25 | } 26 | Error.captureStackTrace(this, DolphFIleUploaderError); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/basic/new.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphServiceHandler } from '../../classes'; 2 | import { Dolph } from '../../common'; 3 | import { DService, OnEvent } from '../../decorators'; 4 | import { EventEmitterService } from '../../packages/events/events_module.packages'; 5 | import { AppService } from './app.service'; 6 | 7 | @DService() 8 | export class NewService extends DolphServiceHandler { 9 | private emitterService: EventEmitterService = new EventEmitterService(); 10 | private AppService: AppService; 11 | 12 | constructor() { 13 | super('newService'); 14 | } 15 | 16 | logger() { 17 | this.emitterService.emitEvent('test'); 18 | console.log('Okay, reached'); 19 | } 20 | 21 | newA() { 22 | return this.AppService.greeting({ name: 'Aemn', age: 12 }); 23 | } 24 | 25 | @OnEvent('test') 26 | test() { 27 | console.log('it works'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "lib": ["ES2017", "esnext.asynciterable"], 6 | "typeRoots": ["node_modules/@types"], 7 | "allowSyntheticDefaultImports": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "module": "commonjs", 13 | "pretty": true, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "outDir": "dist", 17 | "allowJs": true, 18 | "noEmit": false, 19 | "esModuleInterop": true, 20 | "resolveJsonModule": true, 21 | "importHelpers": true, 22 | "baseUrl": "." 23 | }, 24 | "ts-node": { 25 | "compilerOptions": { 26 | "module": "CommonJS" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /decorators/mysql/mysql_initializer.decorators.ts: -------------------------------------------------------------------------------- 1 | import { Model, ModelStatic } from 'sequelize'; 2 | /** 3 | * 4 | * this decorator is used to inject a mongoose model into the serviceHandler as a method 5 | * 6 | * @param propertyName takes the name of the mongoose model 7 | * @param model takes the actual mognoose model imported from the models dir 8 | * 9 | * 10 | * @version 2.0.0 11 | */ 12 | 13 | function InjectMySQL(propertyName: string, model: ModelStatic>) { 14 | return function (constructor: T) { 15 | const Wrapped = class extends constructor { 16 | [propertyName]: ModelStatic> = model; 17 | }; 18 | // Preserving the original class name after extension 19 | Object.defineProperty(Wrapped, 'name', { value: constructor.name, configurable: true }); 20 | return Wrapped; 21 | }; 22 | } 23 | 24 | export { InjectMySQL }; 25 | -------------------------------------------------------------------------------- /samples/basic/event.service.ts: -------------------------------------------------------------------------------- 1 | import { DolphSocketServiceHandler } from '../../classes'; 2 | import { Dolph } from '../../common'; 3 | import { TestEventService } from './test_event_service_dep.service'; 4 | 5 | export class EventService extends DolphSocketServiceHandler { 6 | constructor() { 7 | super(); 8 | this.socketService; 9 | this.handleEvents(); 10 | } 11 | 12 | private TestEventService: TestEventService; 13 | 14 | private handleEvents() { 15 | this.socket.on('connection', (socket) => { 16 | socket.emit('connected', 'connection successful'); 17 | socket.on('new-user', (message) => { 18 | this.socket.emit('new-user', message); 19 | }); 20 | socket.on('data', (data) => { 21 | this.TestEventService.logData(data); 22 | socket.emit('new-user', socket.id); 23 | }); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /classes/event_service_classes.class.ts: -------------------------------------------------------------------------------- 1 | import { DefaultEventsMap } from 'socket.io/dist/typed-events'; 2 | import { Dolph } from '../common'; 3 | import { getInjectedService } from '../core'; 4 | import { SocketService } from '../packages'; 5 | import { Server } from 'socket.io'; 6 | 7 | abstract class DolphSocketServiceHandler { 8 | private _socketService?: SocketService; 9 | public socket?: Server; 10 | 11 | public get socketService(): SocketService { 12 | if (!this._socketService) { 13 | //@ts-expect-error 14 | this._socketService = getInjectedService(SocketService.name); 15 | this.initSocketEvents(); 16 | } 17 | return this._socketService; 18 | } 19 | 20 | private initSocketEvents(): void { 21 | this.socket = this.socketService.getSocket(); 22 | } 23 | } 24 | 25 | export { DolphSocketServiceHandler }; 26 | -------------------------------------------------------------------------------- /decorators/dolph/dolph_service.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DolphConstructor, DolphServiceMapping } from '../../common'; 2 | 3 | /** 4 | * 5 | * Injects an array of `DolphServiceHandler` into a parent class which is then shared by controllers using this services 6 | * 7 | * - top-level class 8 | * @version 1.0.0 9 | */ 10 | function InjectServiceHandler(serivceMappings: DolphServiceMapping[]) { 11 | return function (BaseClass: Base): Base { 12 | return class extends BaseClass { 13 | constructor(...args: any[]) { 14 | super(...args); 15 | for (const mapping of serivceMappings) { 16 | //@ts-expect-error 17 | this[mapping.serviceName] = new mapping.serviceHandler(); 18 | } 19 | } 20 | // initServiceHandlers() { 21 | // } 22 | }; 23 | }; 24 | } 25 | 26 | export { InjectServiceHandler }; 27 | -------------------------------------------------------------------------------- /common/api/SuccessRespone.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from './HttpStatus.api'; 2 | import { ResponseType } from '../types/response.type'; 3 | import { DResponse } from '../interfaces'; 4 | import mime from 'mime-types'; 5 | 6 | /** 7 | * returns a success response with a default `200` status code 8 | * @param {ResponseType} param takes in the `res` ,`status` , `msg` and `body` values 9 | */ 10 | const SuccessResponse = (param: ResponseType): DResponse => { 11 | const contentType = mime.lookup('json') || 'application/json'; 12 | 13 | param.res.set('Content-Type', contentType); 14 | 15 | const { res, status, body, msg } = param; 16 | if (body && msg) res.status(status || HttpStatus.OK).json({ ...body, message: msg }); 17 | if (body) return res.status(status || HttpStatus.OK).json(body); 18 | if (msg) return res.status(status || HttpStatus.OK).send(msg); 19 | return res.status(status || HttpStatus.NO_CONTENT).send(); 20 | }; 21 | 22 | export { SuccessResponse }; 23 | -------------------------------------------------------------------------------- /common/middlewares/default_middleware.middleware.ts: -------------------------------------------------------------------------------- 1 | import { DNextFunc, DRequest, DResponse } from '../interfaces'; 2 | 3 | const DolphAsyncMiddleware = 4 | (fn: (req: DRequest, res: DResponse, next: DNextFunc) => Promise) => 5 | (req: DRequest, res: DResponse, next: DNextFunc) => {}; 6 | 7 | /** 8 | * 9 | * Creates a function that can be used for as an express middleware function 10 | */ 11 | const DolphMiddleware = (fn: any) => (req: DRequest, res: DResponse, next: DNextFunc) => {}; 12 | 13 | /** 14 | * 15 | * Creates an asyncfunction wrapped in a try-catch block which can be used an express middleware function 16 | */ 17 | function DolphAsyncMiddlewareDec(fn: (...args: any[]) => Promise): (...args: any[]) => void { 18 | return function (...args: any[]) { 19 | const [req, res, next] = args; 20 | Promise.resolve(fn(...args)).catch((err) => next(err)); 21 | }; 22 | } 23 | 24 | export { DolphAsyncMiddlewareDec, DolphMiddleware, DolphAsyncMiddleware }; 25 | -------------------------------------------------------------------------------- /utilities/generate_unique_codes.utilities.ts: -------------------------------------------------------------------------------- 1 | const strOne = Date.now().toString(36); 2 | const strTwo = Math.random().toString(36).substring(2); 3 | 4 | function generateRandomCode(digits: number, str: string) { 5 | let code = ''; 6 | while (code.length < digits) { 7 | code += str[Math.floor(Math.random() * str.length)]; 8 | } 9 | return code; 10 | } 11 | 12 | /** 13 | * generates a random 5 digits string 14 | * 15 | * can be used for email validation e.t.c 16 | */ 17 | const uniqueFiveDigitsCode = generateRandomCode(5, strOne + strTwo); 18 | 19 | /** 20 | * generates a random 6 digits string 21 | * 22 | * can be used for email validation e.t.c 23 | */ 24 | const uniqueSixDigitsCode = generateRandomCode(6, strOne + strTwo); 25 | 26 | /** 27 | * generates a random 7 digits string 28 | * 29 | * can be used for email validation e.t.c 30 | */ 31 | const uniqueSevenDigitsCode = generateRandomCode(7, strOne + strTwo); 32 | 33 | export { uniqueFiveDigitsCode, uniqueSevenDigitsCode, uniqueSixDigitsCode }; 34 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | 1. Add the globalExceptionFilter documentation to the docs which means no need to wrap methods with TryCatchAsync anymore 2 | 2. Add notice that the moment library has been removed on release of v2. 3 | 3. The former hashing functions have been deprecated and replaced with new ones, should be added to the README 4 | 4. maxCount field on fileUploader is useless atm and should be worked on 5 | 5. Add documentation for the fileUploader function as MediaParser has been removed 6 | 7 | 8 | ---------- 9 | Write tests for: 10 | 11 | - Routing and Controllers 12 | - Middleware 13 | - Request and Response Handling 14 | - Database Integration 15 | - Error Handling 16 | - Authentication and Authorization 17 | - Testing Edge Cases 18 | - Concurrency and Scalability 19 | - Configuration 20 | - Dependency Injection 21 | - Logging and Debugging 22 | ---------- 23 | 24 | 25 | * const payload: IPayload = { 26 | iat: Math.floor(Date.now() / 1000), 27 | exp: Math.floor(exp.getTime() / 1000), 28 | sub: sub, 29 | info: info, 30 | }; -------------------------------------------------------------------------------- /common/api/ErrorResponse.api.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from './HttpStatus.api'; 2 | import { ResponseType } from '../types/response.type'; 3 | import { DResponse } from '../interfaces'; 4 | import mime from 'mime-types'; 5 | 6 | /** 7 | * returns an error as response with the provided parameters and a default `400` status code 8 | * @param {ResponseType} param takes in the `res` ,`status` , `msg` and `body` values 9 | */ 10 | const ErrorResponse = (param: ResponseType): DResponse => { 11 | const contentType = mime.lookup('json') || 'application/json'; 12 | 13 | param.res.set('Content-Type', contentType); 14 | 15 | const { res, status, body, msg } = param; 16 | if (body && msg) res.status(status || HttpStatus.BAD_REQUEST).json({ ...body, message: msg }); 17 | if (body) return res.status(status || HttpStatus.BAD_REQUEST).json(body); 18 | if (msg) return res.status(status || HttpStatus.BAD_REQUEST).send(msg); 19 | return res.status(status || HttpStatus.INTERNAL_SERVER_ERROR).send(); 20 | }; 21 | 22 | export { ErrorResponse }; 23 | -------------------------------------------------------------------------------- /packages/mongoose/mongoose_connection.package.ts: -------------------------------------------------------------------------------- 1 | import { MongooseConfig } from '../../common'; 2 | import mongoose from 'mongoose'; 3 | import { logger } from '../../utilities'; 4 | import clc from 'cli-color'; 5 | 6 | /** 7 | * 8 | * Used to initialize mongodb with mogoose ORM 9 | * @returns the mogoose promise 10 | * 11 | * @version 1.0.0 12 | */ 13 | const initMongo = (config: MongooseConfig): Promise => { 14 | mongoose.set('strictQuery', false); 15 | return mongoose.connect(config.url, config.options); 16 | }; 17 | 18 | /** 19 | * 20 | * Used to intiialize mongodb with mongoose ORM 21 | * 22 | * Unlike the `initMongo` function, it handles the promise 23 | * 24 | * @version 1.0.0 25 | */ 26 | const autoInitMongo = (config: MongooseConfig): void => { 27 | initMongo(config) 28 | .then((_res) => { 29 | logger.info(clc.blueBright('MONGODB CONNECTED')); 30 | }) 31 | .catch((err) => { 32 | logger.error(clc.red(err)); 33 | }); 34 | }; 35 | 36 | export { initMongo, autoInitMongo }; 37 | -------------------------------------------------------------------------------- /core/morgan.core.ts: -------------------------------------------------------------------------------- 1 | import { DRequest, DResponse } from '../common'; 2 | import morgan from 'morgan'; 3 | import { configs } from './config.core'; 4 | import { logger } from '../utilities'; 5 | import clc from 'cli-color'; 6 | 7 | morgan.token('message', (req: DRequest, res: DResponse) => res.locals.errorMessage || ''); 8 | 9 | const getIpFormat = () => (configs.NODE_ENV !== 'test' ? ':remote-addr - ' : ''); 10 | const successResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms`; 11 | const errorResponseFormat = `${getIpFormat()}:method :url :status - :response-time ms - message: :message`; 12 | 13 | export const successHandler = morgan(successResponseFormat, { 14 | skip: (req: DRequest, res: DResponse) => res.statusCode >= 400, 15 | stream: { write: (message) => logger.info(clc.blackBright(message.trim())) }, 16 | }); 17 | 18 | export const morganErrorHandler = morgan(errorResponseFormat, { 19 | skip: (req: DRequest, res: DResponse) => res.statusCode < 400, 20 | stream: { write: (message) => logger.error(clc.redBright(message.trim())) }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/uploader/remove_uploaded_files.ts: -------------------------------------------------------------------------------- 1 | import { FileHandler, FileInfo, FileRemoveCallback } from '../../common/types/dolph_uploader.type'; 2 | 3 | export function removeUploadedFiles(uploadedFiles: FileInfo[], remove: FileHandler, next: FileRemoveCallback): void { 4 | const length = uploadedFiles.length; 5 | const errors: Error[] = []; 6 | 7 | if (!length) { 8 | return next(null, errors); 9 | } 10 | 11 | function handleFile(index: number): void { 12 | const file = uploadedFiles[index]; 13 | 14 | remove(file, (err: Error | null) => { 15 | if (err) { 16 | const enhancedError = err as Error & { file?: FileInfo; field?: string }; 17 | enhancedError.file = file; 18 | enhancedError.field = file.fieldname; 19 | errors.push(enhancedError); 20 | } 21 | 22 | if (index < length - 1) { 23 | handleFile(index + 1); 24 | } else { 25 | next(null, errors); 26 | } 27 | }); 28 | } 29 | 30 | handleFile(0); 31 | } 32 | -------------------------------------------------------------------------------- /tools/gulp/utilities/helpers.task.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | function isDirectory(path: string): boolean { 5 | return statSync(path).isDirectory(); 6 | } 7 | 8 | export function getFolders(dir: string): string[] { 9 | return readdirSync(dir).filter((file) => isDirectory(join(dir, file))); 10 | } 11 | 12 | export function getDirs(base: string[]): string[] { 13 | const allDirs: string[] = []; 14 | 15 | base.forEach((baseDir) => { 16 | const folders = getFolders(baseDir); 17 | const baseDirsFullPath = folders.map((folder) => join(baseDir, folder)); 18 | // return getFolders(base).map((path) => `${base}/${path}`); 19 | allDirs.push(...baseDirsFullPath); 20 | }); 21 | 22 | return allDirs; 23 | } 24 | 25 | /** 26 | * Checks if the directory contains a package.json file 27 | * @param dir Path to the directory 28 | * @returns True if the directory contains a package.json 29 | */ 30 | export function containsPackageJson(dir: string) { 31 | return readdirSync(dir).some((file) => file === 'package.json'); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The (MIT License) 2 | 3 | Copyright (c) 2023 dolphjs 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 | -------------------------------------------------------------------------------- /core/adapters/mvc_registrar.ts: -------------------------------------------------------------------------------- 1 | import { MVCEngine } from '../../common'; 2 | 3 | class MVCAdapterClass { 4 | private static instance: MVCAdapterClass; 5 | private engine: MVCEngine; 6 | private pathToAssets: string; 7 | private pathToViews: string; 8 | 9 | private constructor() {} 10 | 11 | public static getInstance(): MVCAdapterClass { 12 | if (!MVCAdapterClass.instance) { 13 | MVCAdapterClass.instance = new MVCAdapterClass(); 14 | } 15 | 16 | return MVCAdapterClass.instance; 17 | } 18 | 19 | public setStaticAssets(path: string) { 20 | this.pathToAssets = path; 21 | } 22 | 23 | public setViewsDir(path: string) { 24 | this.pathToViews = path; 25 | } 26 | 27 | public setViewEngine(engine: MVCEngine) { 28 | this.engine = engine; 29 | } 30 | 31 | public getViewsDir() { 32 | return this.pathToViews; 33 | } 34 | 35 | public getViewEngine() { 36 | return this.engine; 37 | } 38 | 39 | public getAssetsPath() { 40 | return this.pathToAssets; 41 | } 42 | } 43 | 44 | export const MVCAdapter = MVCAdapterClass.getInstance(); 45 | -------------------------------------------------------------------------------- /packages/mongoose/transform_doc_plugin.package.ts: -------------------------------------------------------------------------------- 1 | const deleteAtPath = (obj: any, path: any, index: any) => { 2 | if (index === path.length - 1) { 3 | delete obj[path[index]]; 4 | return; 5 | } 6 | deleteAtPath(obj[path[index]], path, index + 1); 7 | }; 8 | 9 | const toJSON = (schema: any) => { 10 | let transform: any; 11 | if (schema.options.toJSON && schema.options.toJSON.transform) { 12 | transform = schema.options.toJSON.transform; 13 | } 14 | 15 | schema.options.toJSON = Object.assign(schema.options.toJSON || {}, { 16 | transform(doc: any, ret: any, options: any) { 17 | Object.keys(schema.paths).forEach((path) => { 18 | if (schema.paths[path].options && schema.paths[path].options.private) { 19 | deleteAtPath(ret, path.split('.'), 0); 20 | } 21 | }); 22 | 23 | ret.id = ret._id.toString(); 24 | delete ret._id; 25 | delete ret.__v; 26 | if (transform) { 27 | return transform(doc, ret, options); 28 | } 29 | }, 30 | }); 31 | }; 32 | 33 | export { toJSON as transformDoc }; 34 | -------------------------------------------------------------------------------- /packages/sequelize/sequelize_connection.package.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../../utilities'; 2 | import clc from 'cli-color'; 3 | import { Sequelize } from 'sequelize'; 4 | 5 | /** 6 | * 7 | * Used to initialize mysql with sequelize ORM 8 | * @returns the mogoose promise 9 | * 10 | * @version 1.0.0 11 | */ 12 | const initMySql = (name: string, user: string, password: string, host: string) => { 13 | const sequelize = new Sequelize(name, user, password, { 14 | dialect: 'mysql', 15 | host: host || 'localhost', 16 | }); 17 | // DolphSequelize = sequelize; 18 | return sequelize; 19 | }; 20 | 21 | /** 22 | * 23 | * Used to intiialize mysql with sequelize ORM 24 | * 25 | * It accepts the return value from the `initSquelize` function and calls the `sync` function on it 26 | * 27 | * @version 1.0.0 28 | */ 29 | const autoInitMySql = (sequelize: Sequelize) => { 30 | sequelize 31 | .sync() 32 | .then((_res) => { 33 | logger.info(clc.blueBright('MYSQL CONNECTED')); 34 | }) 35 | .catch((err: any) => { 36 | logger.error(clc.red(err)); 37 | }); 38 | }; 39 | 40 | export { initMySql, autoInitMySql }; 41 | -------------------------------------------------------------------------------- /core/initializers/dependency_injection.core.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from 'typedi'; 3 | import { logger } from '../../utilities'; 4 | 5 | type injectedServices = { 6 | service: string; 7 | value: any; 8 | }; 9 | 10 | export const GlobalInjection = (service: string, value: any) => { 11 | Container.set(service, value); 12 | }; 13 | 14 | export const getInjectedService = (service: string) => { 15 | return Container.get(service); 16 | }; 17 | 18 | const injectGlobalServices = (services: injectedServices[]) => { 19 | services.forEach((service) => { 20 | try { 21 | Container.set(service.service, service.value); 22 | } catch { 23 | logger.error(`Cannot inject service: ${service.service}`); 24 | } 25 | }); 26 | }; 27 | 28 | export class GlobalInjector { 29 | constructor(services?: injectedServices[]) { 30 | if (services) { 31 | injectGlobalServices(services); 32 | } 33 | } 34 | 35 | register(service: string, value: any) { 36 | injectGlobalServices([{ service, value }]); 37 | } 38 | 39 | get(service: string) { 40 | return getInjectedService(service); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/demo/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "tsx": false, 6 | "dynamicImport": true, 7 | "decorators": true 8 | }, 9 | "transform": { 10 | "legacyDecorator": true, 11 | "decoratorMetadata": true 12 | }, 13 | "target": "es2022", 14 | "externalHelpers": false, 15 | "keepClassNames": true, 16 | "loose": false, 17 | "minify": { 18 | "compress": false, 19 | "mangle": false 20 | }, 21 | "baseUrl": "src", 22 | "paths": { 23 | "@/*": ["*"], 24 | "@/configs/*": ["shared/configs/*"], 25 | "@/components/*": ["components/*"], 26 | "@utils/*": ["shared/utils/*"], 27 | "@shields/*": ["shared/shields/*"], 28 | "@shared/*": ["shared/*"], 29 | "@/helpers*": ["shared/helpers/*"], 30 | "@/interfaces/*": ["shared/interfaces/*"], 31 | "@/middlewares/*": ["shared/middlewares/*"], 32 | "@/decorators/*": ["shared/decorators/*"], 33 | "@/services/*": ["shared/services/*"], 34 | "@/constants/*": ["shared/constants/*"], 35 | "@/validations/*": ["shared/validations/*"] 36 | } 37 | }, 38 | "module": { 39 | "type": "commonjs" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /decorators/validations/validations.decorators.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction } from 'express'; 2 | import { BadRequestException, DNextFunc, DRequest, DResponse, pick } from '../../common'; 3 | import Joi from 'joi'; 4 | 5 | export function ValidateReq(schema: any) { 6 | return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 7 | const originalMethod = descriptor.value; 8 | 9 | descriptor.value = function (req: Request | DRequest, res: Response | DResponse, next: NextFunction | DNextFunc) { 10 | const acceptedSchema = pick(schema, ['param', 'body', 'query']); 11 | const object = pick(req, Object.keys(acceptedSchema)); 12 | const { value, error } = Joi.compile(acceptedSchema) 13 | .prefs({ errors: { label: 'key' }, abortEarly: false }) 14 | .validate(object); 15 | 16 | if (error) { 17 | const errorMessage = error.details.map((details) => details.message).join(', '); 18 | return next(new BadRequestException(errorMessage)); 19 | } 20 | 21 | Object.assign(req, value); 22 | return originalMethod.apply(this, [req, res, next]); 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.shield.ts: -------------------------------------------------------------------------------- 1 | import { DRequest, DResponse, DNextFunc, UnauthorizedException, ForbiddenException } from '../../../../../common'; 2 | import { verifyJWTwithHMAC } from '../../../../../utilities'; 3 | 4 | export const authShield = async (req: DRequest, res: DResponse, next: DNextFunc) => { 5 | try { 6 | let authToken = req.headers['authorization'] as string; 7 | 8 | if (!authToken) { 9 | return next(new UnauthorizedException('Provide a valid authorization token header')); 10 | } 11 | 12 | const bearer = authToken.split(' ')[0]; 13 | 14 | if (bearer !== 'Bearer') return next(new UnauthorizedException('Provide a valid authorization token header')); 15 | 16 | authToken = authToken.split(' ')[1]; 17 | 18 | const payload = verifyJWTwithHMAC({ 19 | token: authToken, 20 | secret: '12357373738388383992ndnd', 21 | }); 22 | 23 | if (!payload) return next(new UnauthorizedException('Invalid or expired token')); 24 | 25 | req.payload = { 26 | ...(payload as any), 27 | }; 28 | 29 | next(); 30 | } catch (e) { 31 | next(new UnauthorizedException(e)); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /utilities/validators/password_validator.validator.ts: -------------------------------------------------------------------------------- 1 | import { DolphErrors } from '../../common/constants'; 2 | import { passwordStrength } from '../../common'; 3 | 4 | /** 5 | * 6 | * default dolphjs password validator 7 | * 8 | * checks for the level of strength of a password 9 | */ 10 | const validatePassword = (level: passwordStrength, value: string, helpers: any) => { 11 | if ((level = 'basic')) { 12 | if (value.length < 6) { 13 | return helpers.message(DolphErrors.passwordShort(6)); 14 | } 15 | } else if ((level = 'medium')) { 16 | if (value.length < 6) { 17 | return helpers.message(DolphErrors.passwordShort(6)); 18 | } 19 | if (value.match(/\d/) || !value.match(/[a-zA-Z]/)) { 20 | return helpers.message(DolphErrors.passwordMustContain(1, 1)); 21 | } 22 | } else if ((level = 'strong')) { 23 | if (value.length < 7) { 24 | return helpers.message(DolphErrors.passwordShort(7)); 25 | } 26 | if (value.match(/\d/) || !value.match(/[a-zA-Z]/)) { 27 | return helpers.message(DolphErrors.passwordMustContain(1, 1)); 28 | } 29 | } 30 | // implement for extra strong 31 | }; 32 | 33 | export { validatePassword }; 34 | -------------------------------------------------------------------------------- /samples/basic/app.server.ts: -------------------------------------------------------------------------------- 1 | import helmet from 'helmet'; 2 | import { DolphFactory, GlobalInjector, middlewareRegistry, MVCAdapter } from '../../core'; 3 | import { SocketService } from '../../packages'; 4 | import { EventEmitterService } from '../../packages/events/events_module.packages'; 5 | import { AppComponent } from './app.component'; 6 | // import { autoInitMySql } from '../../packages'; 7 | import { routes } from './index.routes'; 8 | import { EventsComponent } from './socket.component'; 9 | import path from 'node:path'; 10 | // import { NewController } from './new.controller'; 11 | // import { mysql } from './sqlDb'; 12 | 13 | new GlobalInjector([{ service: EventEmitterService.name, value: new EventEmitterService() }]); 14 | 15 | middlewareRegistry.register(helmet()); 16 | 17 | MVCAdapter.setViewEngine('ejs'); 18 | MVCAdapter.setStaticAssets(path.join(__dirname, 'public')); 19 | 20 | // For Handlebars 21 | // MVCAdapter.setViewsDir(path.join(__dirname, 'views', 'layouts', 'main')); 22 | 23 | MVCAdapter.setViewsDir(path.join(__dirname, 'views')); 24 | 25 | const dolph = new DolphFactory([AppComponent], { 26 | options: { cors: { origin: '*' } }, 27 | socketService: SocketService, 28 | component: new EventsComponent(), 29 | }); 30 | 31 | dolph.start(); 32 | -------------------------------------------------------------------------------- /samples/basic/app.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectMongo, InjectMySQL } from '../../decorators'; 2 | import { DolphServiceHandler } from '../../classes'; 3 | import { IUser, userModel } from './app.model'; 4 | import { Model } from 'mongoose'; 5 | import { User } from './app.schema'; 6 | import { ModelStatic, Model as SqlModel } from 'sequelize'; 7 | import { Dolph } from '../../common'; 8 | import { NewService } from './new.service'; 9 | 10 | @InjectMongo('userModel', userModel) 11 | @InjectMySQL('userMySqlModel', User) 12 | class AppService extends DolphServiceHandler { 13 | userModel!: Model; 14 | userMySqlModel!: ModelStatic>; 15 | NewService: NewService; 16 | 17 | constructor() { 18 | super('appService'); 19 | } 20 | 21 | greeting = (body: { name: string; age: number }) => { 22 | const greeting = `Hi ${body.name}, wow you are ${body.age} years old`; 23 | return greeting; 24 | }; 25 | 26 | createUser = async (body: any) => { 27 | const data = await this.userModel.create(body); 28 | return data; 29 | }; 30 | 31 | createSQLUser = async (body: { username: string; age: string }) => { 32 | return this.userMySqlModel.create(body); 33 | }; 34 | } 35 | 36 | export { AppService }; 37 | -------------------------------------------------------------------------------- /samples/basic/app.router.ts: -------------------------------------------------------------------------------- 1 | import { DolphRouteHandler } from '../../classes'; 2 | import { AppController } from './app.controller'; 3 | import { reqValidatorMiddleware } from '../../common/middlewares'; 4 | import { createUser } from './app.validator'; 5 | import { Dolph } from '../../common'; 6 | import { cookieAuthVerify } from '../../utilities'; 7 | 8 | class AppRouter extends DolphRouteHandler { 9 | constructor() { 10 | super(); 11 | this.initRoutes(); 12 | } 13 | 14 | public readonly path: string = '/app'; 15 | controller: AppController = new AppController(); 16 | 17 | initRoutes() { 18 | this.router.post(`${this.path}`, this.controller.sendGreeting); 19 | this.router.post(`${this.path}/user`, reqValidatorMiddleware(createUser), this.controller.createUser); 20 | this.router.post(`${this.path}/register`, this.controller.register); 21 | this.router.post(`${this.path}/sql`, this.controller.testMysql); 22 | this.router.get(`${this.path}/cookie`, this.controller.testCookieFn); 23 | // this.router.get(`${this.path}/cookie-validation`, cookieAuthVerify('random_secret'), this.controller.testCookieVerify); 24 | this.router.get(`${this.path}/cookie-validation`, this.controller.testCookieVerify); 25 | } 26 | } 27 | 28 | export { AppRouter }; 29 | -------------------------------------------------------------------------------- /samples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "main": "app/server.js", 5 | "author": "", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">=18.0.0" 9 | }, 10 | "scripts": { 11 | "dev:start": "dolph watch", 12 | "dev:docker:start": "docker-compose -f docker-compose-dev.yml up", 13 | "dev:docker:stop": "docker-compose -f docker-compose-dev.yml down", 14 | "build": "dolph build", 15 | "build:tsc": "tsc && tsc-alias", 16 | "start": "dolph watch", 17 | "clean": "rm -r app && rm -r logs" 18 | }, 19 | "dependencies": { 20 | "@dolphjs/dolph": "1.3.7", 21 | "class-transformer": "^0.5.1", 22 | "class-validator": "^0.14.1", 23 | "handlebars": "^4.7.8", 24 | "helmet": "^7.1.0", 25 | "joi": "^17.12.3", 26 | "mjml": "^4.15.3", 27 | "nodemailer": "^6.9.13", 28 | "pug": "^3.0.3" 29 | }, 30 | "devDependencies": { 31 | "@swc/cli": "^0.1.62", 32 | "@swc/core": "^1.3.91", 33 | "@types/express": "^4.17.21", 34 | "@types/node": "^20.8.2", 35 | "@types/nodemailer": "^6.4.14", 36 | "graphql-scalars": "^1.23.0", 37 | "ts-node": "^10.9.1", 38 | "tsc-alias": "^1.8.8", 39 | "tsconfig-paths": "^4.2.0", 40 | "typescript": "^5.2.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /common/middlewares/validate_request_middleware.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { BadRequestException, pick } from '../api'; 3 | import Joi = require('joi'); 4 | import { DNextFunc, DRequest, DResponse } from '../interfaces'; 5 | 6 | /** 7 | * 8 | * dolphjs middleware used to validate a request object form the client 9 | * 10 | * it's used in conjunction with JOI in order to work 11 | * 12 | * provides validation for `body`, `params` & `query` objects 13 | * 14 | * - see [https://github.com/dolphjs/dolph#README] for usage 15 | * @version 1.0.0 16 | */ 17 | const validateRequestMiddleware = 18 | (schema: any) => (req: Request | DRequest, res: Response | DResponse, next: NextFunction | DNextFunc) => { 19 | const acceptedSchema = pick(schema, ['param', 'body', 'query']); 20 | const object = pick(req, Object.keys(acceptedSchema)); 21 | const { value, error } = Joi.compile(acceptedSchema) 22 | .prefs({ errors: { label: 'key' }, abortEarly: false }) 23 | .validate(object); 24 | 25 | if (error) { 26 | const errorMessage = error.details.map((details) => details.message).join(', '); 27 | return next(new BadRequestException(errorMessage)); 28 | } 29 | 30 | Object.assign(req, value); 31 | return next(); 32 | }; 33 | 34 | export { validateRequestMiddleware as reqValidatorMiddleware }; 35 | -------------------------------------------------------------------------------- /samples/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "sourceMap": false, 9 | "useUnknownInCatchVariables": false, 10 | "strictFunctionTypes": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "target": "ES2022", 15 | "baseUrl": "src", 16 | "outDir": "app", 17 | "pretty": true, 18 | "module": "commonjs", 19 | "paths": { 20 | "@/*": ["*"], 21 | "@/configs/*": ["shared/configs/*"], 22 | "@/components/*": ["components/*"], 23 | "@utils/*": ["shared/utils/*"], 24 | "@shields/*": ["shared/shields/*"], 25 | "@shared/*": ["shared/*"], 26 | "@/helpers*": ["shared/helpers/*"], 27 | "@/interfaces/*": ["shared/interfaces/*"], 28 | "@/middlewares/*": ["shared/middlewares/*"], 29 | "@/decorators/*": ["shared/decorators/*"], 30 | "@/services/*": ["shared/services/*"], 31 | "@/constants/*": ["shared/constants/*"], 32 | "@/validations/*": ["shared/validations/*"] 33 | } 34 | }, 35 | "include": ["src/", ".env"], 36 | "ts-node": { 37 | "compilerOptions": { 38 | "module": "CommonJS" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /decorators/others/cookie_auth_verify.decorators.ts: -------------------------------------------------------------------------------- 1 | import { DNextFunc, DRequest, DResponse, ErrorException, HttpStatus, IPayload } from '../../common'; 2 | import { verifyJWTwithHMAC } from '../../utilities'; 3 | 4 | /** 5 | * 6 | * class-method decorator used for authorization based on the dolphjs default cookie authentication and authorization pattern 7 | * 8 | * @version 1.0 9 | */ 10 | export const CookieAuthVerifyDec = (tokenSecret: string) => { 11 | return (_target: any, _propertyKey: string, descriptor?: TypedPropertyDescriptor) => { 12 | const originalMethod = descriptor.value; 13 | 14 | // convert to normal func 15 | 16 | descriptor.value = (req: DRequest, res: DResponse, next: DNextFunc) => { 17 | try { 18 | const context = this; 19 | 20 | if (!req.cookies) 21 | return next(new ErrorException('user not authorized, login and try again', HttpStatus.UNAUTHORIZED)); 22 | 23 | const { xAuthToken } = req.cookies; 24 | 25 | if (!xAuthToken) 26 | return next(new ErrorException('user not authorized, login and try again', HttpStatus.UNAUTHORIZED)); 27 | 28 | let payload: IPayload; 29 | 30 | //@ts-expect-error 31 | payload = verifyJWTwithHMAC({ token: xAuthToken, secret: tokenSecret }); 32 | 33 | req.payload = payload; 34 | 35 | return originalMethod.apply(context, [req, res, next]); 36 | } catch (e) { 37 | throw e; 38 | } 39 | }; 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /common/types/dolph.types.ts: -------------------------------------------------------------------------------- 1 | import { DolphConstructor } from '..'; 2 | 3 | export type DolphDbs = 'mongo' | 'postgre' | 'mysql' | 'sqllite' | 'maria' | 'cassandra'; 4 | export type DolphMiddlewareOption = { 5 | activate?: boolean | undefined; 6 | origin?: string | undefined; 7 | allowedHeaders?: string[] | undefined | null; 8 | maxAge?: number | undefined; 9 | exposedHeaders?: string[] | null | undefined; 10 | credentials?: boolean | undefined; 11 | preflightContinue?: boolean | undefined; 12 | optionsSuccessStatus: number | undefined; 13 | }; 14 | 15 | export type DolphMiddlewareHelmetOption = { 16 | activate: boolean; 17 | contentSecurityPolicy?: { 18 | directives?: { 19 | defaultSrc?: string[]; 20 | scriptSrc?: string[]; 21 | }; 22 | }; 23 | expectCt?: { 24 | enforce?: boolean; 25 | maxAge?: number; 26 | }; 27 | featurePolicy?: { 28 | features?: { 29 | fullscreen?: string[]; 30 | vibrate?: string[]; 31 | }; 32 | }; 33 | referrerPolicy?: { 34 | policy?: string; 35 | }; 36 | hsts?: { 37 | maxAge?: number; 38 | includeSubDomains?: boolean; 39 | }; 40 | crossOriginEmbedderPolicy?: { 41 | policy?: string; 42 | }; 43 | crossOriginOpenerPolicy?: { 44 | policy?: string; 45 | }; 46 | }; 47 | 48 | export type DolphServiceMapping = { 49 | serviceName: keyof T; 50 | serviceHandler: DolphConstructor; 51 | }; 52 | 53 | export type Dolph = string; 54 | 55 | export type MVCEngine = 'handlebars' | 'pug' | 'ejs'; 56 | -------------------------------------------------------------------------------- /packages/memdb/memdb.packages.ts: -------------------------------------------------------------------------------- 1 | export class MemDB { 2 | data: Record; 3 | 4 | constructor() { 5 | this.data = {}; 6 | } 7 | 8 | /** 9 | * Adds a new key-value pair to the database. 10 | * If the key is not a string, it will be converted to a string using JSON.stringify. 11 | */ 12 | add(key: string, value: T) { 13 | if (this.get(key)) throw new Error('cannot add new value because `key` already exists. '); 14 | this.data[key] = value; 15 | } 16 | 17 | /** 18 | * Get a value from the database using the key. 19 | */ 20 | get(key: string) { 21 | return this.data[key]; 22 | } 23 | 24 | /** 25 | * Remove data from the database. 26 | * @returns the data if found, otherwise throws an error. 27 | */ 28 | remove(key: string) { 29 | if (!(key in this.data)) return undefined; 30 | 31 | const data = this.data[key]; 32 | delete this.data[key]; 33 | return data; 34 | } 35 | 36 | /** 37 | * Checks if data exists in the database then returns it. 38 | */ 39 | has(key: string) { 40 | return key in this.data; 41 | } 42 | 43 | /** 44 | * Returns all the keys in the database. 45 | */ 46 | getAllKeys() { 47 | return Object.keys(this.data); 48 | } 49 | 50 | /** 51 | * Returns all the values in the database. 52 | */ 53 | getAllValues() { 54 | return Object.values(this.data); 55 | } 56 | 57 | /** 58 | * Clears the database. 59 | */ 60 | empty() { 61 | this.data = {}; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import {} from '../decorators'; 2 | import { DRequest, DResponse, DNextFunc, validateBodyMiddleware } from '../common'; 3 | import { IsString, Length } from 'class-validator'; 4 | 5 | class TestDto { 6 | @IsString() 7 | @Length(1, 10) 8 | name: string; 9 | } 10 | 11 | describe('Validation Middleware', () => { 12 | let req: Partial; 13 | let res: Partial; 14 | let next: DNextFunc; 15 | 16 | beforeEach(() => { 17 | req = { body: { name: 'test' } }; 18 | 19 | res = { 20 | status: jest.fn().mockReturnThis(), 21 | json: jest.fn(), 22 | }; 23 | 24 | next = jest.fn(); 25 | }); 26 | 27 | it('should call next if validation succeeds', async () => { 28 | req.body = { name: 'validName' }; 29 | const middleware = validateBodyMiddleware(TestDto); 30 | await middleware(req as DRequest, res as DResponse, next); 31 | expect(next).toHaveBeenCalled(); 32 | expect(res.status).not.toHaveBeenCalled(); 33 | expect(res.json).not.toHaveBeenCalled(); 34 | }); 35 | 36 | it('should return 400 if validation fails', async () => { 37 | req.body = { name: '' }; 38 | const middleware = validateBodyMiddleware(TestDto); 39 | await middleware(req as DRequest, res as DResponse, next); 40 | expect(res.status).toHaveBeenCalledWith(400); 41 | expect(res.json).toHaveBeenCalledWith( 42 | expect.objectContaining({ message: 'Validation failed', error: expect.any(String) }), 43 | ); 44 | expect(next).not.toHaveBeenCalled(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /classes/controller_classes.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * serviceHandlers Array takes in an object which has a name for a key and the serviceHandler as value 3 | * Example: [{'userService': new UserService()}] 4 | */ 5 | 6 | import { Dolph } from '../common'; 7 | 8 | /** 9 | * `DolphControllerHandler` takes a string generic. 10 | * 11 | */ 12 | // Note: string generic would be useful in the future 13 | abstract class DolphControllerHandler {} 14 | 15 | export { DolphControllerHandler }; 16 | /** 17 | * Example of use 18 | */ 19 | 20 | /** 21 | * serviceHandlers Array takes in an object which has a name for a key and the serviceHandler as value 22 | * Example: [{'userService': new UserService()}] 23 | */ 24 | 25 | // import { DolphServiceHandler } from "./service_classes.class"; 26 | 27 | // abstract class DolphControllerHandler{ 28 | // abstract path: string; 29 | // abstract serviceHandlers: Array> 30 | // getHandler(name: string){ 31 | // return this.serviceHandlers.find((handler)=> handler.name === name) 32 | // } 33 | // } 34 | 35 | // class NewO extends DolphServiceHandler<"services">{ 36 | // getUser = ()=>{ 37 | // console.log("It is working"); 38 | // } 39 | // } 40 | 41 | // class NewH extends DolphControllerHandler{ 42 | // path: string = "/home"; 43 | // serviceHandlers: DolphServiceHandler[] = [new NewO("services")]; 44 | 45 | // CreateUser = ()=>{ 46 | // const service = this.getHandler("service") 47 | // if(service instanceof NewO){ 48 | // service.getUser(); 49 | // } 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user2.controller.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler } from '../../../../../classes'; 2 | import { Dolph, DRequest, DResponse, IPayload, SuccessResponse } from '../../../../../common'; 3 | import { DBody, DPayload, DReq, DRes, Get, Post, Route, UseMiddleware } from '../../../../../decorators'; 4 | import { generateJWTwithHMAC } from '../../../../../utilities'; 5 | import { CreateUserDto } from './user.dto'; 6 | import { authShield } from './user.shield'; 7 | import { User2Service } from './user2.service'; 8 | 9 | @Route('user2') 10 | export class User2Controller extends DolphControllerHandler { 11 | private User2Service: User2Service; 12 | constructor() { 13 | super(); 14 | } 15 | 16 | @Get('') 17 | get(req: DRequest, res: DResponse) { 18 | return req.query; 19 | } 20 | 21 | @UseMiddleware(authShield) 22 | @Post('') 23 | async print(@DReq() req: DRequest, @DRes() res: DResponse, @DPayload() payload: IPayload) { 24 | console.log(payload); 25 | 26 | SuccessResponse({ res, body: payload }); 27 | } 28 | 29 | @Post('login') 30 | async login(@DRes() res, @DBody(CreateUserDto) body: CreateUserDto) { 31 | const accessToken = this.signToken(body.email, new Date(Date.now() + 3000 * 1000)); 32 | SuccessResponse({ res, body: accessToken, ...body }); 33 | } 34 | 35 | private signToken(userId: string, expires: Date): string { 36 | const payload: IPayload = { 37 | sub: userId, 38 | iat: Math.floor(Date.now() / 1000), 39 | exp: Math.floor(expires.getTime() / 1000), 40 | }; 41 | 42 | return generateJWTwithHMAC({ payload, secret: '12357373738388383992ndnd' }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /decorators/events/event.decorator.ts: -------------------------------------------------------------------------------- 1 | import { getInjectedService } from '../../core'; 2 | import { EventEmitterService } from '../../packages/events/events_module.packages'; 3 | 4 | /** 5 | * - currently not released 6 | * @version 2.0 7 | */ 8 | function OnEvent(eventName: string, priority: number = 0, once: boolean = false) { 9 | return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { 10 | const originalMethod = descriptor.value; 11 | 12 | descriptor.value = function (...args: any[]) { 13 | const eventEmitter: EventEmitterService = getInjectedService(EventEmitterService.name) as any; 14 | 15 | eventEmitter.emitEvent(eventName, originalMethod.bind(this), priority, once); 16 | return originalMethod.apply(this, args); 17 | }; 18 | 19 | return descriptor; 20 | }; 21 | } 22 | 23 | /** 24 | * - currently not released 25 | * @version 2.0 26 | */ 27 | function OnceEvent(eventName: string, priority: number = 0) { 28 | return OnEvent(eventName, priority, true); 29 | } 30 | 31 | /** 32 | * - currently not released 33 | * @version 2.0 34 | */ 35 | function OffEvent(eventName: string) { 36 | return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { 37 | const originalMethod = descriptor.value; 38 | 39 | descriptor.value = function (...args: any[]) { 40 | const eventEmitter: EventEmitterService = getInjectedService(EventEmitterService.name) as any; 41 | 42 | eventEmitter.unregisterEvent(eventName, originalMethod.bind(this)); 43 | return originalMethod.apply(this, args); 44 | }; 45 | 46 | return descriptor; 47 | }; 48 | } 49 | 50 | export { OnEvent, OnceEvent, OffEvent }; 51 | -------------------------------------------------------------------------------- /tests/uploader/memorystorage.test.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { memoryStorage } from '../../packages'; 3 | import { DRequest } from '../../common'; 4 | 5 | describe('Memory Storage Test', () => { 6 | let storage: ReturnType; 7 | 8 | beforeEach(() => { 9 | storage = memoryStorage(); 10 | }); 11 | 12 | test('should store file in memory correctly', (done) => { 13 | const testContent = 'test content'; 14 | const mockFile = { 15 | fieldname: 'testFile', 16 | originalname: 'test-file.txt', 17 | encoding: '7bit', 18 | mimetype: 'text/plain', 19 | stream: Readable.from(Buffer.from(testContent)), 20 | }; 21 | 22 | const mockRequest = {} as DRequest; 23 | 24 | storage.handleFile(mockRequest, mockFile, (error, info) => { 25 | expect(error).toBeNull(); 26 | expect(info?.buffer).toBeDefined(); 27 | expect(info?.size).toBe(testContent.length); 28 | expect(info?.buffer?.toString()).toBe(testContent); 29 | done(); 30 | }); 31 | }); 32 | 33 | test('should remove file from memory correctly', (done) => { 34 | const mockFile = { 35 | fieldname: 'testFile', 36 | originalname: 'test-file.txt', 37 | encoding: '7bit', 38 | mimetype: 'text/plain', 39 | buffer: Buffer.from('test content'), 40 | }; 41 | 42 | const mockRequest = {} as DRequest; 43 | 44 | storage.removeFile(mockRequest, mockFile, (error) => { 45 | expect(error).toBeNull(); 46 | expect(mockFile).not.toHaveProperty('buffer'); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /decorators/sockets/sockets.decorators.ts: -------------------------------------------------------------------------------- 1 | import clc from 'cli-color'; 2 | import { DolphServiceHandler, DolphSocketServiceHandler } from '../../classes'; 3 | import { Dolph, SocketServicesParams } from '../../common'; 4 | import { logger } from '../../utilities'; 5 | 6 | function isDolphServiceHandler(classOrInstance: any): classOrInstance is typeof DolphServiceHandler { 7 | return classOrInstance.prototype instanceof DolphServiceHandler || classOrInstance === DolphServiceHandler; 8 | } 9 | 10 | function isDolphSocketServiceHandler(classOrInstance: any): classOrInstance is typeof DolphSocketServiceHandler { 11 | return classOrInstance.prototype instanceof DolphSocketServiceHandler || classOrInstance === DolphSocketServiceHandler; 12 | } 13 | 14 | export const Socket = ({ services, socketServices }: SocketServicesParams): ClassDecorator => { 15 | if (Array.isArray(socketServices)) { 16 | return (target: any) => { 17 | socketServices.forEach((socketService) => { 18 | services.forEach((service) => { 19 | const serviceInstance = new service(); 20 | const serviceName = service.name; 21 | 22 | Object.defineProperty(socketService.prototype, serviceName, { 23 | value: serviceInstance, 24 | writable: true, 25 | configurable: true, 26 | enumerable: true, 27 | }); 28 | }); 29 | }); 30 | 31 | Reflect.defineMetadata('sockets', socketServices, target.prototype); 32 | }; 33 | } else { 34 | logger.error(clc.red('Provide an array of socket services with type `new (): DolphSocketServiceHandler` ')); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | plugins: ['@typescript-eslint/eslint-plugin'], 7 | extends: ['plugin:@typescript-eslint/recommended', 'prettier'], 8 | overrides: [ 9 | { 10 | files: ['**/*.ts'], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | project: 'tsconfig.json', 14 | sourceType: 'module', 15 | }, 16 | rules: { 17 | '@typescript-eslint/interface-name-prefix': 'off', 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/explicit-module-boundary-types': 'off', 21 | '@typescript-eslint/no-unused-vars': 'on', 22 | '@typescript-eslint/ban-types': 'off', 23 | }, 24 | }, 25 | { 26 | files: ['**/*.spec.ts', 'integration/**/*.ts'], 27 | parser: '@typescript-eslint/parser', 28 | parserOptions: { 29 | project: 'tsconfig.spec.json', 30 | sourceType: 'module', 31 | }, 32 | rules: { 33 | '@typescript-eslint/interface-name-prefix': 'off', 34 | '@typescript-eslint/explicit-function-return-type': 'off', 35 | '@typescript-eslint/no-explicit-any': 'off', 36 | '@typescript-eslint/explicit-module-boundary-types': 'off', 37 | '@typescript-eslint/no-unused-vars': 'on', 38 | '@typescript-eslint/ban-types': 'off', 39 | '@typescript-eslint/no-empty-function': 'off', 40 | }, 41 | }, 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /samples/basic/new.controller.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler } from '../../classes'; 2 | import { 3 | DRequest, 4 | DResponse, 5 | Dolph, 6 | SuccessResponse, 7 | validateBodyMiddleware, 8 | validateParamMiddleware, 9 | validateQueryMiddleware, 10 | } from '../../common'; 11 | import { 12 | Body, 13 | Get, 14 | Post, 15 | Render, 16 | Route, 17 | Shield, 18 | UnShield, 19 | UseMiddleware as UseMiddleware, 20 | ValidateReq, 21 | } from '../../decorators'; 22 | import { testMiddleware } from './app.middleware'; 23 | import { testCase } from './app.validator'; 24 | import { CreateUserDto } from './new.dto'; 25 | import { NewService } from './new.service'; 26 | 27 | @Shield(testMiddleware) 28 | @Route('app') 29 | export class NewController extends DolphControllerHandler { 30 | private NewService: NewService; 31 | 32 | constructor() { 33 | super(); 34 | } 35 | 36 | @Get('test') 37 | async newReq(req: DRequest, res: DResponse) { 38 | this.NewService.logger(); 39 | const a = this.NewService.newA(); 40 | SuccessResponse({ res, body: { message: a } }); 41 | } 42 | 43 | @Post('test') 44 | // @UseMiddleware(testMiddleware) 45 | // @ValidateReq(testCase) 46 | @UnShield(testMiddleware) 47 | @UseMiddleware(validateBodyMiddleware(CreateUserDto)) 48 | async testNewController(req: DRequest, res: DResponse) { 49 | const dto: CreateUserDto = req.body; 50 | SuccessResponse({ res, body: { dto, ...req.payload } }); 51 | } 52 | 53 | @Get('home') 54 | @Render('home') 55 | getHomePage(req: DRequest, res: DResponse) { 56 | return { title: 'Home' }; 57 | } 58 | 59 | @Get('about') 60 | @Render('about') 61 | getAboutPage(req: DRequest, res: DResponse) { 62 | return { title: 'About' }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/error.core.ts: -------------------------------------------------------------------------------- 1 | import { DNextFunc, DRequest, DResponse } from '../common'; 2 | import mongoose from 'mongoose'; 3 | import httpStatus from 'http-status'; 4 | import { configs } from './config.core'; 5 | import { logger } from '../utilities'; 6 | import clc from 'cli-color'; 7 | import { DefaultException } from '../common/api/exceptions/default_exception.api'; 8 | 9 | export const errorConverter = (err: any, req: DRequest, res: DResponse, next: DNextFunc) => { 10 | let error = err; 11 | 12 | if (!(error instanceof DefaultException)) { 13 | const statusCode = 14 | error.statusCode || 15 | (error instanceof mongoose.Error ? httpStatus.BAD_REQUEST : httpStatus.INTERNAL_SERVER_ERROR); 16 | const message = error.message || httpStatus[statusCode]; 17 | error = new DefaultException(message, statusCode, false, err.stack); 18 | } 19 | 20 | next(error); 21 | }; 22 | 23 | export const errorHandler = (err: any, req: DRequest, res: DResponse, next: DNextFunc) => { 24 | let statusCode = err.statusCode || httpStatus.INTERNAL_SERVER_ERROR; 25 | let message = err.message || 'An unexpected server error occurred'; 26 | 27 | if (configs.NODE_ENV === 'production' && !err.isOperational) { 28 | if (!statusCode) { 29 | statusCode = httpStatus.INTERNAL_SERVER_ERROR; 30 | message = 'Internal Server Error'; 31 | } 32 | } 33 | 34 | res.locals.errorMessage = message; 35 | 36 | const response = { 37 | code: statusCode, 38 | message, 39 | ...(configs.NODE_ENV === 'development' && { stack: err.stack }), 40 | }; 41 | 42 | if (configs.NODE_ENV === 'test') { 43 | logger.error(clc.red(err)); 44 | } 45 | 46 | // TODO: remove this to allow for other content types 47 | res.set('Content-Type', 'application/json'); 48 | res.status(statusCode).json(response); 49 | }; 50 | -------------------------------------------------------------------------------- /tests/fallback_middleware.test.ts: -------------------------------------------------------------------------------- 1 | import { DRequest, DRequestHandler } from '../common'; 2 | import { middlewareRegistry } from '../core'; 3 | import helmet from 'helmet'; 4 | 5 | describe('MiddlewareRegistry', () => { 6 | beforeEach(() => { 7 | // Reset the singleton instance and clear middlewares before each test 8 | (middlewareRegistry as any).middlewares = []; 9 | }); 10 | 11 | it('should return a singleton instance', () => { 12 | const instance1 = middlewareRegistry; 13 | const instance2 = middlewareRegistry; 14 | expect(instance1).toBe(instance2); 15 | }); 16 | 17 | it('should register a middleware', () => { 18 | const mockMiddleware: DRequestHandler = (req, res, next) => next(); 19 | middlewareRegistry.register(mockMiddleware); 20 | 21 | const middlewares = middlewareRegistry.getMiddlewares(); 22 | expect(middlewares.length).toBe(1); 23 | expect(middlewares[0]).toBe(mockMiddleware); 24 | }); 25 | 26 | it('should register multiple middlewares', () => { 27 | const mockMiddleware1: DRequestHandler = (req, res, next) => next(); 28 | const mockMiddleware2: DRequestHandler = (req, res, next) => next(); 29 | middlewareRegistry.register(mockMiddleware1); 30 | middlewareRegistry.register(mockMiddleware2); 31 | 32 | const middlewares = middlewareRegistry.getMiddlewares(); 33 | expect(middlewares.length).toBe(2); 34 | expect(middlewares).toContain(mockMiddleware1); 35 | expect(middlewares).toContain(mockMiddleware2); 36 | }); 37 | 38 | it('should register and retrieve helmet middleware', () => { 39 | middlewareRegistry.register(helmet()); 40 | 41 | const middlewares = middlewareRegistry.getMiddlewares(); 42 | expect(middlewares.length).toBe(1); 43 | expect(typeof middlewares[0]).toBe('function'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /utilities/encryptions/bcrypt_encryption.utilities.ts: -------------------------------------------------------------------------------- 1 | import { genSalt, hashSync, compareSync } from 'bcryptjs'; 2 | import { bcryptCompareParam, bcryptHashParam } from '../../common'; 3 | import { boolean } from 'joi'; 4 | 5 | /** 6 | * 7 | * @param param0 takes the `salt` and `pureString` params and if the salt param is not given, a 11 round salt is generated by default 8 | * @returns resulting hash 9 | * 10 | * @version 1.0.0 11 | * @deprecated use `hashString` instead 12 | */ 13 | const hashWithBcrypt = async ({ pureString, salt }: bcryptHashParam): Promise => { 14 | return hashSync(pureString, salt ? salt : await genSalt(11)); 15 | }; 16 | 17 | /** 18 | * 19 | * @param pureString this is the string to be hashed 20 | * @param salt this is the salt. In the absence of the field, an 11 round salt is generated by default 21 | * @returns resulting hash 22 | * 23 | * @version 2.0 24 | */ 25 | const hashString = async (pureString: string, salt: number = 11): Promise => { 26 | return hashSync(pureString, await genSalt(salt)); 27 | }; 28 | 29 | /** 30 | * 31 | * @param param0 takes the `pureString` and `hashString` params 32 | * @returns {Boolean} true/false depending on the outcome of comparison 33 | * 34 | * @version 1.0.0 35 | * @deprecated use `compareHashedString` instead 36 | * 37 | */ 38 | const compareWithBcryptHash = ({ pureString, hashString }: bcryptCompareParam): boolean => { 39 | return compareSync(pureString, hashString); 40 | }; 41 | 42 | /** 43 | * 44 | * @param pureString this is the string to br compared against 45 | * @param hashString this is the previously hashed string 46 | * @returns {Boolean} 47 | * 48 | * @version 2.0 49 | */ 50 | const compareHashedString = (pureString: string, hashString: string): boolean => { 51 | return compareSync(pureString, hashString); 52 | }; 53 | 54 | export { compareWithBcryptHash, hashWithBcrypt, compareHashedString, hashString }; 55 | -------------------------------------------------------------------------------- /utilities/auth/JWT_generator.utilities.ts: -------------------------------------------------------------------------------- 1 | import JWT from 'jsonwebtoken'; 2 | import { IPayload } from '../../common'; 3 | import { readFileSync } from 'fs'; 4 | 5 | /** 6 | * 7 | * Generates JWT auth token using the HMAC algorithm 8 | * 9 | * @version 1.0.0 10 | */ 11 | const generateJWTwithHMAC = ({ payload, secret }: { payload: IPayload; secret: string }): string => { 12 | const token = JWT.sign(payload, secret); 13 | return token; 14 | }; 15 | 16 | /** 17 | * 18 | * Verify's JWT auth token created using the HMAC algorithm 19 | * 20 | * @version 1.0.0 21 | */ 22 | const verifyJWTwithHMAC = ({ token, secret }: { token: string | string[]; secret: string }): string | JWT.JwtPayload => { 23 | //@ts-expect-error 24 | const payload = JWT.verify(token, secret); 25 | return payload; 26 | }; 27 | 28 | /** 29 | * 30 | * Generates JWT token using the RSA algorithm and a privatekey 31 | * 32 | * @version 1.0.0 33 | */ 34 | const generateJWTwithRSA = ({ pathToPrivateKey, payload }: { pathToPrivateKey: string; payload: IPayload }): string => { 35 | const privateKey = readFileSync(pathToPrivateKey, 'utf8'); 36 | const token = JWT.sign(payload, privateKey, { algorithm: 'RS256' }); 37 | return token; 38 | }; 39 | 40 | /** 41 | * 42 | * Verify's JWT token created using the `generateJWTwithRSA` function and publicKey 43 | * 44 | * @version 1.0.0 45 | */ 46 | const verifyJWTwithRSA = ({ 47 | pathToPublicKey, 48 | token, 49 | }: { 50 | pathToPublicKey: string; 51 | token: string | string[]; 52 | }): string | JWT.JwtPayload => { 53 | const publicKey = readFileSync(pathToPublicKey, 'utf8'); 54 | try { 55 | //@ts-expect-error 56 | const payload = JWT.verify(token, publicKey, { algorithms: ['RS256'] }); 57 | return payload; 58 | } catch (e) { 59 | throw e; 60 | } 61 | }; 62 | 63 | export { generateJWTwithHMAC, verifyJWTwithHMAC, verifyJWTwithRSA, generateJWTwithRSA }; 64 | -------------------------------------------------------------------------------- /samples/demo/src/components/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler } from '../../../../../classes'; 2 | import { 3 | Dolph, 4 | SuccessResponse, 5 | DRequest, 6 | DResponse, 7 | InternalServerErrorException, 8 | Middleware, 9 | } from '../../../../../common'; 10 | import { Get, Post, Route, UseMiddleware } from '../../../../../decorators'; 11 | import { diskStorage, fileUploader, useFileUploader } from '../../../../../packages'; 12 | import { UserService } from './user.service'; 13 | import { User2Service } from './user2.service'; 14 | 15 | @Route('user') 16 | export class UserController extends DolphControllerHandler { 17 | private UserService: UserService; 18 | private User2Service: User2Service; 19 | 20 | constructor() { 21 | super(); 22 | } 23 | 24 | @Get('greet') 25 | async greet(req: DRequest, res: DResponse) { 26 | const result1 = this.UserService.createUser(12, 'hello'); 27 | 28 | const result2 = this.User2Service.get(); 29 | 30 | SuccessResponse({ 31 | res, 32 | body: { message: "you've reached the user endpoint.", resultOne: result1, resultTwo: result2 }, 33 | }); 34 | } 35 | 36 | // Make a wrapper that returns the function : req: DRequest, res: DResponse, next: DNextFunc 37 | 38 | @Post('') 39 | @UseMiddleware( 40 | useFileUploader({ 41 | type: 'array', 42 | fieldname: 'upload', 43 | maxCount: 2, 44 | 45 | // storage: diskStorage({ 46 | // destination: './uploads', 47 | // filename: (req, file, cb) => { 48 | // cb(null, Date.now() + '-' + file.originalname); 49 | // }, 50 | // }), 51 | }), 52 | ) 53 | async post(req: DRequest, res: DResponse) { 54 | console.log('req.files: ', req.file); 55 | SuccessResponse({ res, body: req.body }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/transformer.ts: -------------------------------------------------------------------------------- 1 | import { ClassConstructor, plainToInstance } from 'class-transformer'; 2 | import { validate, ValidationError } from 'class-validator'; 3 | import clc from 'cli-color'; 4 | import { logger } from '../utilities'; 5 | import { ValidationException } from './adapters/exception_handlers'; 6 | 7 | export async function transformAndValidateDto( 8 | dtoClass: ClassConstructor | undefined, 9 | plainObject: any, 10 | // e.g., "request body", "query parameters" 11 | contextDescription: string, 12 | ): Promise { 13 | // Ensure dtoClass is a valid class constructor for DTOs 14 | if (!dtoClass) return; 15 | if (typeof dtoClass !== 'function' || [String, Number, Boolean, Object].includes(dtoClass as any)) { 16 | // Object constructor itself is not a DTO 17 | // This scenario should ideally be caught by the @DBody decorator's type checking. 18 | // However, if it somehow gets here, it's an internal error or misconfiguration. 19 | logger.error(clc.red(`Invalid DTO class provided for ${contextDescription}. Received:, dtoClass`)); 20 | throw new Error(`Dolph: Misconfigured DTO type for ${contextDescription}.`); 21 | } 22 | 23 | const dtoInstance = plainToInstance(dtoClass, plainObject, { 24 | enableImplicitConversion: true, 25 | }); 26 | 27 | const errors: ValidationError[] = await validate(dtoInstance, { 28 | // Remove properties that do not have any validation decorators 29 | whitelist: true, 30 | // Throw an error if non-whitelisted properties are present 31 | forbidNonWhitelisted: true, 32 | // validationError: { target: false }, // Optionally hide the target object in errors 33 | }); 34 | 35 | if (errors.length > 0) { 36 | logger.error(clc.red(`Validation failed for ${contextDescription}:`), errors); 37 | throw new ValidationException(errors, `Validation failed for ${contextDescription}`); 38 | } 39 | 40 | return dtoInstance; 41 | } 42 | -------------------------------------------------------------------------------- /common/interfaces/dolph.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { DolphMiddlewareOption, dolphEnv, dolphPort } from '..'; 2 | import { IPayload, MongooseConfig } from '.'; 3 | import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; 4 | import { IncomingMessage, ServerResponse } from 'http'; 5 | import { FileInfo, UploadConfig } from '../types/dolph_uploader.type'; 6 | 7 | export interface DolphConfig { 8 | database?: DolphConfigDbOption; 9 | middlewares?: DolphConfigMiddlewares; 10 | port?: dolphPort; 11 | routing?: DolphConfigRouting; 12 | env?: dolphEnv; 13 | jsonLimit?: string; 14 | globalExceptionFilter?: boolean; 15 | } 16 | 17 | export interface DolphConfigDbOption { 18 | mongo: MongooseConfig; 19 | mysql: MySqlConfig; 20 | } 21 | 22 | export interface MySqlConfig { 23 | host: string; 24 | database: string; 25 | user: string; 26 | pass?: string | null; 27 | } 28 | 29 | export interface DolphConfigMiddlewares { 30 | cors?: DolphMiddlewareOption; 31 | xss?: DolphMiddlewareOption; 32 | } 33 | 34 | export interface DolphConfigRouting { 35 | base?: string; 36 | } 37 | 38 | export interface DolphConstructor { 39 | new (...args: any[]): T; 40 | } 41 | 42 | export type DRequest = Omit & 43 | IncomingMessage & { 44 | /** 45 | * stores the value of token payload 46 | */ 47 | payload?: IPayload; 48 | body?: Record; 49 | file?: FileInfo; 50 | files?: FileInfo[] | Record; 51 | uploadConfig?: UploadConfig; 52 | }; 53 | 54 | export type DResponse = Response & 55 | ServerResponse & { 56 | body?: T; 57 | }; 58 | 59 | export interface DRequestHandler extends RequestHandler {} 60 | 61 | export type DNextFunc = NextFunction & {}; 62 | 63 | export interface DRouter extends Router {} 64 | 65 | export interface OtherParams { 66 | passportConfigs?: passportConfigs; 67 | } 68 | 69 | export interface passportConfigs { 70 | passportStrategy?: any; 71 | passportSerializer?: any; 72 | } 73 | -------------------------------------------------------------------------------- /packages/uploader/index.ts: -------------------------------------------------------------------------------- 1 | export { DolphFIleUploaderError as DFileUploaderError } from './errors/error_messages'; 2 | import { extname } from 'path'; 3 | import { DRequest, DResponse } from '../../common'; 4 | import { FileInfo, UploadOptions } from '../../common/types/dolph_uploader.type'; 5 | import { defaultFileExtensions } from '../../utilities/media/file_extensions.utilities'; 6 | import { diskStorage, fileUploader, memoryStorage } from './file_uploader'; 7 | 8 | /** 9 | * diskStorage({ 10 | destination: './uploads', 11 | filename: (req, file, cb) => { 12 | cb(null, Date.now() + '-' + file.originalname); 13 | }, 14 | */ 15 | 16 | /** 17 | * Use this middleware to process media files 18 | * @version 1.4.0 19 | * 20 | * @author Utee 21 | * 22 | */ 23 | export const useFileUploader = 24 | ({ storage, fileFilter, extensions, type, fieldname, fields, limit, maxCount }: UploadOptions) => 25 | (req, res: DResponse, next) => { 26 | let _filter = fileFilter; 27 | 28 | if (!_filter) { 29 | let _extensions = extensions?.length ? extensions : defaultFileExtensions; 30 | 31 | _filter = (req: DRequest, file: FileInfo, callback) => { 32 | const extensionCheck = _extensions.includes(extname(file.originalname).toLowerCase()); 33 | 34 | if (!extensionCheck && file.originalname !== 'blob') { 35 | return callback(new Error('Unsupported media file'), false); 36 | } 37 | 38 | return callback(null, true); 39 | }; 40 | } 41 | 42 | // The bug is here at the fieldname upload 43 | 44 | const uploadMiddleware = fileUploader({ 45 | storage: storage || memoryStorage(), 46 | limits: { 47 | fileSize: limit || 5 * 1024 * 1024, // 5MB 48 | }, 49 | fieldname: fieldname || 'upload', 50 | fileFilter: _filter, 51 | type, 52 | maxCount, 53 | fields, 54 | }); 55 | 56 | uploadMiddleware(req, res, next); 57 | }; 58 | 59 | export { fileUploader, diskStorage, memoryStorage }; 60 | -------------------------------------------------------------------------------- /utilities/auth/cookie.utilities.ts: -------------------------------------------------------------------------------- 1 | import { configs } from '../../core/config.core'; 2 | import { DNextFunc, DRequest, DResponse, ErrorException, HttpStatus, IPayload, cookieContent, sub } from '../../common'; 3 | import { generateJWTwithHMAC, verifyJWTwithHMAC } from './JWT_generator.utilities'; 4 | 5 | /** 6 | * 7 | * @param sub holds value for the principal data used in hashing the token 8 | * @param exp expiration time in Date 9 | * @param secret token secret used for authorization 10 | * @param info option parameter used to store data that might be useful 11 | * 12 | * @version 2 13 | */ 14 | export const newAuthCookie = (sub: sub, exp: Date, secret: string, info?: string | object | []): cookieContent => { 15 | const payload: IPayload = { 16 | iat: Math.floor(Date.now() / 1000), 17 | exp: Math.floor(exp.getTime() / 1000), 18 | sub: sub, 19 | info: info, 20 | }; 21 | const token = generateJWTwithHMAC({ payload, secret }); 22 | 23 | const options = { 24 | expires: new Date(exp.getTime()), 25 | httpOnly: false, 26 | secure: false, 27 | }; 28 | 29 | if (configs.NODE_ENV !== 'development') { 30 | options.httpOnly = true; 31 | options.secure = true; 32 | } 33 | 34 | return { 35 | name: 'xAuthToken', 36 | value: token, 37 | expires: options.expires, 38 | httpOnly: options.httpOnly, 39 | secure: options.secure, 40 | }; 41 | }; 42 | 43 | /** 44 | * function used for authorization based on the dolphjs default cookie authentication and authorization pattern 45 | * 46 | * @version 1.1 47 | */ 48 | export const cookieAuthVerify = (tokenSecret: string) => (req: DRequest, _res: DResponse, next: DNextFunc) => { 49 | const cookies = req.cookies; 50 | 51 | if (!cookies) return next(new ErrorException('user not authorized, login and try again', HttpStatus.UNAUTHORIZED)); 52 | 53 | const { xAuthToken } = cookies; 54 | 55 | if (!xAuthToken) return next(new ErrorException('user not authorized, login and try again', HttpStatus.UNAUTHORIZED)); 56 | 57 | let payload: IPayload; 58 | 59 | //@ts-expect-error 60 | payload = verifyJWTwithHMAC({ token: xAuthToken, secret: tokenSecret }); 61 | 62 | req.payload = payload; 63 | next(); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/uploader/file_appender.ts: -------------------------------------------------------------------------------- 1 | import { DRequest } from '../../common'; 2 | import { FileInfo, FileStrategy } from '../../common/types/dolph_uploader.type'; 3 | 4 | function arrayRemove(arr: T[], item: T): void { 5 | const index = arr.indexOf(item); 6 | if (index !== -1) arr.splice(index, 1); 7 | } 8 | 9 | export class FileAppender { 10 | constructor( 11 | private strategy: FileStrategy, 12 | private request: DRequest, 13 | ) { 14 | switch (strategy) { 15 | case 'ARRAY': 16 | this.request.files = []; 17 | break; 18 | case 'OBJECT': 19 | this.request.files = Object.create(null); 20 | break; 21 | } 22 | } 23 | 24 | insertPlaceholder(file: FileInfo): Partial { 25 | const placeholder = { fieldname: file.fieldname }; 26 | 27 | switch (this.strategy) { 28 | case 'ARRAY': 29 | (this.request.files as FileInfo[]).push(placeholder as FileInfo); 30 | break; 31 | case 'OBJECT': 32 | const files = this.request.files as Record; 33 | if (files[file.fieldname]) { 34 | files[file.fieldname].push(placeholder as FileInfo); 35 | } else { 36 | files[file.fieldname] = [placeholder as FileInfo]; 37 | } 38 | break; 39 | } 40 | 41 | return placeholder; 42 | } 43 | 44 | removePlaceholder(placeholder: Partial): void { 45 | switch (this.strategy) { 46 | case 'ARRAY': 47 | const files = this.request.files as FileInfo[]; 48 | arrayRemove(files, placeholder); 49 | break; 50 | case 'OBJECT': 51 | const filesObj = this.request.files as Record; 52 | if (filesObj[placeholder.fieldname!].length === 1) { 53 | delete filesObj[placeholder.fieldname!]; 54 | } else { 55 | arrayRemove(filesObj[placeholder.fieldname!], placeholder); 56 | } 57 | break; 58 | } 59 | } 60 | 61 | replacePlaceholder(placeholder: Partial, file: FileInfo): void { 62 | if (this.strategy === 'VALUE') { 63 | this.request.file = file; 64 | return; 65 | } 66 | delete placeholder.fieldname; 67 | Object.assign(placeholder, file); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /common/middlewares/try_catch_middleware.middleware.ts: -------------------------------------------------------------------------------- 1 | import { DNextFunc, DRequest, DResponse } from '../interfaces'; 2 | 3 | /** 4 | * 5 | * Creates an asynchronous function wrapped in a try-catch block 6 | * 7 | * This is the recommended function to be used for controllers and services that need try-catch 8 | * 9 | * @version 1.0.0 10 | */ 11 | const TryCatchAsyncFn = 12 | (fn: (req: DRequest, res: DResponse, next: DNextFunc) => Promise) => 13 | (req: DRequest, res: DResponse, next: DNextFunc) => { 14 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)); 15 | }; 16 | 17 | /** 18 | * 19 | * An asynchronous class-method decorator which wraps a method in a try-catch block 20 | * 21 | * Should be used as a top-most decorator 22 | * @version 1.0.0 23 | * @deprecated - see @dolphjs/dolph/decorators 24 | */ 25 | const TryCatchAsyncDec = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 26 | const originalMethod = descriptor.value; 27 | 28 | descriptor.value = async function (...args: any[]) { 29 | const context = this; 30 | // console.log(this); 31 | const [req, res, next] = args; 32 | try { 33 | await originalMethod.apply(context, args); 34 | } catch (err) { 35 | next(err); 36 | } 37 | }; 38 | 39 | // return descriptor; 40 | }; 41 | 42 | /** 43 | * 44 | * A class-method decorator which wraps a method in a try-catch block 45 | * 46 | * Should be used as a top-most decorator 47 | * 48 | * @version 1.0.4 49 | */ 50 | const TryCatchDec = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 51 | const originalMethod = descriptor.value; 52 | 53 | descriptor.value = function (...args: any[]) { 54 | const context = this; 55 | // console.log(this); 56 | const [req, res, next] = args; 57 | try { 58 | originalMethod.apply(context, args); 59 | } catch (err) { 60 | next(err); 61 | } 62 | }; 63 | 64 | // return descriptor; 65 | }; 66 | 67 | /** 68 | * 69 | * Creates a function wrapped in a try-catch block 70 | * 71 | * This is the recommended function to be used for controllers and services that don't use try-catch blocks 72 | * 73 | * @version 1.0.0 74 | */ 75 | const TryCatchFn = 76 | (fn: (req: DRequest, res: DResponse, next: DNextFunc) => void) => (req: DRequest, res: DResponse, next: DNextFunc) => { 77 | Promise.resolve(fn(req, res, next)).catch((err) => next(err)); 78 | }; 79 | 80 | export { TryCatchAsyncFn, TryCatchFn, TryCatchAsyncDec, TryCatchDec }; 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the DolphJs Docs! 4 | 5 | ## Code of Conduct 6 | 7 | DolphJs has adopted a code of conduct that we expect contributors to adhere to. 8 | Please read the instructions in the [CODE_OF_CONDUCT](CODE_OF_CONDUCT) so 9 | you can understand our ethics. 10 | 11 | **Try to follow your own instructions.** 12 | 13 | When writing step-by-step instructions (e.g. how to install something), try to forget everything you know about the topic, and actually follow the instructions you wrote, a single step at time. Often you will discover that there is implicit knowledge that you forgot to mention, or that there are missing or out-of-order steps in the instructions. Bonus points for getting _somebody else_ to follow the steps and watching what they struggle with. Often it would be something very simple that you have not anticipated. 14 | 15 | ## Guidelines for Code Examples 16 | 17 | ### Syntax 18 | 19 | #### Prefer OOP to Functional Programming 20 | 21 | DolphJS is strictly built with the OOP approach. 22 | 23 | #### Use `const` where possible, otherwise `let`. Don't use `var` 24 | 25 | #### Don't use ES5 features when equivalent ES6 features have no downsides 26 | 27 | #### Don't forget to remove console.log's when finished with them 28 | 29 | #### Ensure that your code is scalable and uses the best implementation e.g prefer code with 0(n) to 0(logn) 30 | 31 | In particular, you should prefer arrow functions `const myFunction = () => ...` to named `functions` for top-level functions. 32 | 33 | ### Package Manager 34 | 35 | - Use only yarn 36 | 37 | ### Style 38 | 39 | - Use semicolons. 40 | - No space between function names and parenthesis (`method() {}` not `method () {}`). 41 | - When in doubt, use the default style favored by [Prettier](https://prettier.io/playground/). 42 | 43 | ### Highlighting 44 | 45 | Use `typescript` as the highlighting language in Markdown code blocks: 46 | 47 | ```` 48 | ```typescript 49 | // code 50 | ``` 51 | ```` 52 | 53 | Sometimes you'll see blocks with numbers. 54 | They tell the website to highlight specific lines. 55 | 56 | You can highlight a single line: 57 | 58 | ```` 59 | ```javascipt {2} 60 | function hello() { 61 | // this line will get highlighted 62 | } 63 | ``` 64 | ```` 65 | 66 | A range of lines: 67 | 68 | ```` 69 | ```typescript {2-4} 70 | function hello() { 71 | // these lines 72 | // will get 73 | // highlighted 74 | } 75 | ``` 76 | ```` 77 | 78 | Or even multiple ranges: 79 | 80 | ```` 81 | ```typescript {2-4,6} 82 | function hello() { 83 | // these lines 84 | // will get 85 | // highlighted 86 | console.log('hello'); 87 | // also this one 88 | console.log('there'); 89 | } 90 | ``` 91 | ```` 92 | 93 | Be mindful that if you move some code in an example with highlighting, you also need to update the highlighting. 94 | 95 | Don't be afraid to often use highlighting! It is very valuable when you need to focus the reader's attention on a particular detail that's easy to miss. 96 | -------------------------------------------------------------------------------- /packages/events/events_module.packages.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'node:events'; 2 | import { Service } from 'typedi'; 3 | import { Listener } from '../../common'; 4 | 5 | @Service() 6 | export class EventEmitterService extends EventEmitter { 7 | private listenerRegistry: Map = new Map(); 8 | 9 | constructor() { 10 | super(); 11 | } 12 | 13 | emitEvent>(eventName: string, ...args: T): boolean { 14 | let listeners = this.listenerRegistry.get(eventName) || []; 15 | let stopPropagation = false; 16 | 17 | for (const entry of listeners) { 18 | const result = entry.listener(...args); 19 | if (entry.once) { 20 | this.offEvent(eventName, entry.listener); 21 | } 22 | if (result === false) { 23 | stopPropagation = true; 24 | break; 25 | } 26 | } 27 | 28 | // Cleanup listeners marked for one-time use 29 | listeners = listeners.filter((listener) => !listener.once); 30 | this.listenerRegistry.set(eventName, listeners); 31 | 32 | return !stopPropagation; 33 | } 34 | 35 | onEvent void | boolean>( 36 | eventName: string, 37 | listener: T, 38 | priority: number = 0, 39 | once: boolean = false, 40 | ): void { 41 | const listeners = this.listenerRegistry.get(eventName) || []; 42 | listeners.push({ listener, priority, once }); 43 | listeners.sort((a, b) => b.priority - a.priority); 44 | this.listenerRegistry.set(eventName, listeners); 45 | if (once) { 46 | super.once(eventName, listener as (...args: any[]) => void); 47 | } else { 48 | super.on(eventName, listener as (...args: any[]) => void); 49 | } 50 | } 51 | 52 | onceEvent void>(eventName: string, listener: T, priority: number = 0): void { 53 | this.onEvent(eventName, listener, priority, true); 54 | } 55 | 56 | offEvent void>(eventName: string, listener: T): void { 57 | const listeners = this.listenerRegistry.get(eventName) || []; 58 | const index = listeners.findIndex((l) => l.listener === listener); 59 | if (index !== -1) { 60 | listeners.splice(index, 1); 61 | this.listenerRegistry.set(eventName, listeners); 62 | } 63 | super.off(eventName, listener); 64 | } 65 | 66 | removeAllListenersForEvent(eventName: string): void { 67 | this.listenerRegistry.delete(eventName); 68 | super.removeAllListeners(eventName); 69 | } 70 | 71 | unregisterEvent(eventName: string, listener: (...args) => void) { 72 | this.removeListener(eventName, listener); 73 | let listeners = this.listenerRegistry.get(eventName) || []; 74 | listeners = listeners.filter((l) => l.listener !== listener); 75 | this.listenerRegistry.set(eventName, listeners); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/uploader/diskstorage.test.ts: -------------------------------------------------------------------------------- 1 | import { unlinkSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { Readable } from 'stream'; 4 | import { diskStorage } from '../../packages'; 5 | import { DRequest } from '../../common'; 6 | 7 | describe('Disk Storage Test', () => { 8 | const testDestination = join(__dirname, 'uploads'); 9 | let storage: ReturnType; 10 | 11 | beforeEach(() => { 12 | storage = diskStorage({ destination: testDestination }); 13 | }); 14 | 15 | afterEach(() => { 16 | try { 17 | unlinkSync(join(testDestination, 'test-file.txt')); 18 | } catch (err) { 19 | // Ignore cleanup errors 20 | } 21 | }); 22 | 23 | test('should handle file upload correctly', (done) => { 24 | const mockFile = { 25 | fieldname: 'testFile', 26 | originalname: 'test-file.txt', 27 | encoding: '7bit', 28 | mimetype: 'text/plain', 29 | stream: Readable.from(Buffer.from('test content')), 30 | }; 31 | 32 | const mockRequest = {} as DRequest; 33 | 34 | storage.handleFile(mockRequest, mockFile, (error, info) => { 35 | expect(error).toBeNull(); 36 | expect(info).toBeDefined(); 37 | expect(info?.destination).toBe(testDestination); 38 | expect(info?.filename).toBeDefined(); 39 | expect(info?.path).toBeDefined(); 40 | expect(info?.size).toBe(12); // length of 'test content' 41 | done(); 42 | }); 43 | }); 44 | 45 | test('should remove file correctly', (done) => { 46 | const filePath = join(testDestination, 'test-file.txt'); 47 | const mockFile = { 48 | fieldname: 'testFile', 49 | originalname: 'test-file.txt', 50 | encoding: '7bit', 51 | mimetype: 'text/plain', 52 | path: filePath, 53 | }; 54 | 55 | const mockRequest = {} as DRequest; 56 | 57 | storage.removeFile(mockRequest, mockFile, (error) => { 58 | expect(error).toBeNull(); 59 | expect(mockFile).not.toHaveProperty('path'); 60 | expect(mockFile).not.toHaveProperty('destination'); 61 | expect(mockFile).not.toHaveProperty('filename'); 62 | done(); 63 | }); 64 | }); 65 | 66 | test('should handle custom filename function', (done) => { 67 | const customStorage = diskStorage({ 68 | destination: testDestination, 69 | filename: (_req, file, cb) => { 70 | cb(null, `${file.originalname}`); 71 | }, 72 | }); 73 | 74 | const mockFile = { 75 | fieldname: 'testFile', 76 | originalname: 'test-file.txt', 77 | encoding: '7bit', 78 | mimetype: 'text/plain', 79 | stream: Readable.from(Buffer.from('test content')), 80 | }; 81 | 82 | const mockRequest = {} as DRequest; 83 | 84 | customStorage.handleFile(mockRequest, mockFile, (error, info) => { 85 | expect(error).toBeNull(); 86 | expect(info?.filename).toBe('test-file.txt'); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /common/types/dolph_uploader.type.ts: -------------------------------------------------------------------------------- 1 | import { DRequest } from '../interfaces'; 2 | 3 | export interface FileInfo { 4 | fieldname: string; 5 | originalname: string; 6 | encoding: string; 7 | mimetype: string; 8 | destination?: string; 9 | filename?: string; 10 | path?: string; 11 | size?: number; 12 | buffer?: Buffer; 13 | stream?: NodeJS.ReadableStream; 14 | } 15 | 16 | export interface UploadConfig { 17 | limits?: Record; 18 | preservePaths?: boolean; 19 | storage: Storage; 20 | fileFilter: FileFilter; 21 | fileStrategy: FileStrategy; 22 | } 23 | 24 | export type UploadOptionAndConfig = UploadConfig & Omit & {}; 25 | 26 | export type FileFilter = ( 27 | req: DRequest, 28 | file: FileInfo, 29 | callback: (error: Error | null, acceptFile: boolean) => void, 30 | ) => void; 31 | 32 | export type FileStrategy = 'NONE' | 'VALUE' | 'ARRAY' | 'OBJECT'; 33 | 34 | export type UploadOptions = { 35 | /** 36 | * accepts either **diskStorage** or **memoryStorage** 37 | * 38 | * defaults to *memoryStorage* 39 | * 40 | * @type {Storage} 41 | */ 42 | storage?: Storage; 43 | /** 44 | * the name that file(s) sent to the dolph app will be identified by 45 | * 46 | * defaults to *upload* 47 | */ 48 | fieldname?: string; 49 | /** 50 | * the limit in fileSize for media being processed 51 | * 52 | * defaults to *5MB* 53 | */ 54 | limit?: number; 55 | 56 | /** 57 | * type of media process to use 58 | * 59 | * **single** accepts only one file 60 | * 61 | * 62 | * **array** accepts an array of files 63 | * 64 | * only when set to **array** does **maxCount** matter 65 | * 66 | * **fields** accepts fields/object of files 67 | * 68 | */ 69 | type: 'single' | 'fields' | 'array'; 70 | 71 | /** 72 | * @type {FileFilter} 73 | * 74 | * defaults to use the default *FileFilter* function which should suit most use-cases 75 | */ 76 | fileFilter?: FileFilter; 77 | 78 | /** 79 | * specify what file extensions should be allowed in this format: **[".png", ".pdf"]** 80 | */ 81 | extensions?: string[]; 82 | 83 | /** 84 | * max count of files that can be uploaded 85 | * 86 | * only works when `type` is of *array* 87 | */ 88 | maxCount?: number; 89 | 90 | /** 91 | * @type {UploadFields} 92 | */ 93 | fields?: UploadFields[]; 94 | }; 95 | 96 | export type UploadFields = { 97 | name: string; 98 | maxCount?: number; 99 | }; 100 | 101 | export interface Storage { 102 | handleFile: (req: DRequest, file: FileInfo, callback: (error: Error | null, info?: Partial) => void) => void; 103 | removeFile: (req: DRequest, file: FileInfo, callback: (error: Error | null) => void) => void; 104 | } 105 | 106 | export interface FileRemoveCallback { 107 | (error: Error | null, storageErrors?: Error[]): void; 108 | } 109 | export interface FileHandler { 110 | (file: FileInfo, callback: (error: Error | null) => void): void; 111 | } 112 | 113 | export interface DiskStorageOptions { 114 | destination?: 115 | | string 116 | | ((req: DRequest, file: FileInfo, callback: (error: Error | null, destination: string) => void) => void); 117 | filename?: (req: DRequest, file: FileInfo, callback: (error: Error | null, filename: string) => void) => void; 118 | } 119 | -------------------------------------------------------------------------------- /samples/basic/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { DolphControllerHandler, DolphServiceHandler, JWTAuthVerifyDec, JWTAuthorizeDec } from '../../classes'; 2 | import { TryCatchAsyncDec, TryCatchAsyncFn, TryCatchFn } from '../../common/middlewares'; 3 | import { AppService } from './app.service'; 4 | import { BadRequestException, DNextFunc, DRequest, DResponse, Dolph, SuccessResponse } from '../../common'; 5 | import { generateJWTwithHMAC, newAuthCookie } from '../../utilities'; 6 | import moment from 'moment'; 7 | import { MediaParser } from '../../utilities'; 8 | import { CookieAuthVerifyDec, InjectServiceHandler } from '../../decorators'; 9 | import { authFunc } from './app.auth_function'; 10 | 11 | @InjectServiceHandler([{ serviceHandler: AppService, serviceName: 'appservice' }]) 12 | class ControllerService { 13 | appservice!: AppService; 14 | } 15 | 16 | const controllerServices = new ControllerService(); 17 | class AppController extends DolphControllerHandler { 18 | constructor() { 19 | super(); 20 | this.createUser = this.createUser.bind(this); 21 | } 22 | 23 | public readonly sendGreeting = TryCatchFn((req: DRequest, res: DResponse) => { 24 | const { body } = req; 25 | const response = controllerServices.appservice.greeting(body); 26 | SuccessResponse({ res, msg: response }); 27 | }); 28 | 29 | @TryCatchAsyncDec 30 | @JWTAuthVerifyDec('random_secret') 31 | // @JWTAuthorizeDec('random_secret', authFunc) 32 | @MediaParser({ fieldname: 'upload', type: 'single', extensions: ['.png'] }) 33 | public async createUser(req: DRequest, res: DResponse) { 34 | const { body, file } = req; 35 | if (body.height < 1.7) throw new BadRequestException('sorry, you are too short for this program'); 36 | const data = await controllerServices.appservice.createUser(body); 37 | SuccessResponse({ res, body: { data, file: file, payload: req.payload } }); 38 | } 39 | 40 | public readonly register = TryCatchAsyncFn(async (req: DRequest, res: DResponse, next: DNextFunc) => { 41 | const { username } = req.body; 42 | console.log(req.file); 43 | const token = generateJWTwithHMAC({ 44 | payload: { 45 | exp: moment().add(30000, 'seconds').unix(), 46 | iat: moment().unix(), 47 | sub: username, 48 | }, 49 | secret: 'random_secret', 50 | }); 51 | SuccessResponse({ res, body: token }); 52 | }); 53 | 54 | @TryCatchAsyncDec 55 | public async testMysql(req: DRequest, res: DResponse) { 56 | const { username, age } = req.body; 57 | console.log(username, age); 58 | const result = await controllerServices.appservice.createSQLUser({ username, age }); 59 | SuccessResponse({ res, body: result }); 60 | } 61 | 62 | @TryCatchAsyncDec 63 | public async testCookieFn(req: DRequest, res: DResponse) { 64 | const cookieValue = newAuthCookie('user_id_00', 100000, 'random_secret', { info: 'yeah' }); 65 | res.cookie(cookieValue.name, cookieValue.value, { 66 | expires: cookieValue.expires, 67 | secure: cookieValue.secure, 68 | httpOnly: cookieValue.httpOnly, 69 | }); 70 | 71 | res.send('done'); 72 | } 73 | 74 | @TryCatchAsyncDec 75 | @CookieAuthVerifyDec('random_secret') 76 | public async testCookieVerify(req: DRequest, res: DResponse) { 77 | const payload = req.payload; 78 | 79 | res.json(payload); 80 | } 81 | } 82 | // const appController = new AppController(); 83 | export { AppController }; 84 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | 77 | -------------------------------------------------------------------------------- /common/middlewares/global_validation_middleware.middlewares.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { plainToClass } from 'class-transformer'; 3 | import { DNextFunc, DRequest, DResponse } from '../interfaces'; 4 | import { validate, ValidationError } from 'class-validator'; 5 | 6 | // does not work for current use 7 | export const validationMiddlewareOne = async (req: DRequest, res: DResponse, next: DNextFunc) => { 8 | const routeHandler = req.route?.stack?.find((layer) => layer.method === req.method.toLowerCase())?.handle; 9 | const dto = Reflect.getMetadata('dto', routeHandler); 10 | 11 | if (!dto) { 12 | return next(); 13 | } 14 | 15 | const bodyObject = plainToClass(dto, req.body); 16 | const queryObject = plainToClass(dto, req.query); 17 | const paramObject = plainToClass(dto, req.params); 18 | 19 | const errors: ValidationError[] = await Promise.all([ 20 | validate(bodyObject), 21 | validate(queryObject), 22 | validate(paramObject), 23 | ]).then((results) => [].concat(...results)); 24 | 25 | if (errors.length > 0) { 26 | const constraints = errors 27 | .map((error) => Object.values(error.constraints || {})) 28 | .reduce((acc, val) => acc.concat(val), []); 29 | return res.status(400).json({ message: 'Validation failed', errors: constraints }); 30 | } 31 | 32 | if (req.body) { 33 | req.body = bodyObject; 34 | } 35 | 36 | if (req.params) { 37 | req.params = paramObject; 38 | } 39 | 40 | if (req.query) { 41 | req.query = queryObject; 42 | } 43 | 44 | next(); 45 | }; 46 | 47 | export const validateBodyMiddleware = (dto: any) => { 48 | return async (req: DRequest, res: DResponse, next: DNextFunc) => { 49 | const object = plainToClass(dto, req.body); 50 | 51 | const errors: ValidationError[] = await validate(object); 52 | 53 | if (errors.length > 0) { 54 | const constraints = errors 55 | .map((error) => Object.values(error.constraints || {})) 56 | .reduce((acc, val) => acc.concat(val), []) 57 | .join(', '); 58 | return res.status(400).json({ message: 'Validation failed', error: constraints }); 59 | } else { 60 | req.body = object; 61 | next(); 62 | } 63 | }; 64 | }; 65 | 66 | export const validateQueryMiddleware = (dto: any) => { 67 | return async (req: DRequest, res: DResponse, next: DNextFunc) => { 68 | const object = plainToClass(dto, req.query); 69 | 70 | const errors: ValidationError[] = await validate(object); 71 | 72 | if (errors.length > 0) { 73 | const constraints = errors 74 | .map((error) => Object.values(error.constraints || {})) 75 | .reduce((acc, val) => acc.concat(val), []) 76 | .join(', '); 77 | return res.status(400).json({ message: 'Validation failed', error: constraints }); 78 | } else { 79 | req.body = object; 80 | next(); 81 | } 82 | }; 83 | }; 84 | 85 | export const validateParamMiddleware = (dto: any) => { 86 | return async (req: DRequest, res: DResponse, next: DNextFunc) => { 87 | const object = plainToClass(dto, req.query); 88 | 89 | const errors: ValidationError[] = await validate(object); 90 | 91 | if (errors.length > 0) { 92 | const constraints = errors 93 | .map((error) => Object.values(error.constraints || {})) 94 | .reduce((acc, val) => acc.concat(val), []) 95 | .join(', '); 96 | return res.status(400).json({ message: 'Validation failed', error: constraints }); 97 | } else { 98 | req.body = object; 99 | next(); 100 | } 101 | }; 102 | }; 103 | -------------------------------------------------------------------------------- /utilities/logger.utilities.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import clc from 'cli-color'; 3 | import { formatDate } from './formatters/format_date.utilities'; 4 | 5 | const logger = winston.createLogger({ 6 | levels: { 7 | error: 0, 8 | warn: 1, 9 | info: 2, 10 | debug: 3, 11 | }, 12 | format: winston.format.combine( 13 | winston.format.timestamp(), 14 | winston.format.printf((info) => { 15 | const { timestamp, level, message, stack } = info; 16 | const formattedTimestamp = clc.white(formatDate(timestamp)); 17 | let formattedLevel: string; 18 | 19 | let newMessage: string = message; 20 | 21 | if (level === 'info') { 22 | formattedLevel = clc.blue(`[${level.toUpperCase()}]`); 23 | } else if (level === 'warn') { 24 | formattedLevel = clc.yellow(`[${level.toUpperCase()}]`); 25 | newMessage = clc.yellowBright(message); 26 | } else if (level === 'debug') { 27 | formattedLevel = clc.green(`[${level.toUpperCase()}]`); 28 | newMessage = clc.greenBright(message); 29 | } else if (level === 'error') { 30 | formattedLevel = clc.red(`[${level.toUpperCase()}]`); 31 | newMessage = clc.redBright(message); 32 | } 33 | 34 | let logMessage = `${formattedLevel}: ${formattedTimestamp} ${newMessage}`; 35 | 36 | if (stack) { 37 | logMessage += `\n${stack}`; 38 | } 39 | 40 | return logMessage; 41 | }), 42 | ), 43 | 44 | transports: [new winston.transports.Console({ level: 'debug' })], 45 | }); 46 | 47 | const emojiForLevel = { 48 | info: '🔹', 49 | warn: '⚠️', 50 | debug: '🐛', 51 | error: '❗️', 52 | }; 53 | 54 | // const componentEmoji = { 55 | // route: '🌐', 56 | // shield: '🛡️', 57 | // middleware: '🛡️', 58 | // }; 59 | 60 | const inAppLogger = winston.createLogger({ 61 | levels: { 62 | error: 0, 63 | warn: 1, 64 | info: 2, 65 | debug: 3, 66 | }, 67 | format: winston.format.combine( 68 | winston.format.timestamp(), 69 | winston.format.printf((info) => { 70 | const { timestamp, level, message } = info; 71 | const formattedTimestamp = formatDate(timestamp); 72 | const logEmoji = emojiForLevel[level] || '🔹'; 73 | let formattedMessage = message; 74 | 75 | let levelLog = `${clc.blue([`${level.toUpperCase()}`])}`; 76 | if (level === 'warn') { 77 | levelLog = `${clc.yellow([`${level}`])}`; 78 | } else if (level === 'debug') { 79 | levelLog = `${clc.cyan([`${level}`])}`; 80 | } else if (level === 'error') { 81 | levelLog = `${clc.red([`${level}`])}`; 82 | } 83 | 84 | if (message.startsWith('Registered')) { 85 | // Split the message to extract the relevant parts 86 | const [action, componentDetails] = message.split('>>>>').map((part) => part.trim()); 87 | 88 | // Determine the component type based on the presence of specific keywords 89 | // const componentType = componentDetails.includes('Middleware') 90 | // ? 'middleware' 91 | // : componentDetails.includes('Shield') 92 | // ? 'shield' 93 | // : 'route'; // Default to 'route' if neither Middleware nor Shield is found 94 | 95 | // Construct the formatted message 96 | formattedMessage = `${logEmoji} [${formattedTimestamp}] ${levelLog}: ${clc.blueBright( 97 | `${action}`, 98 | )} ${clc.blueBright(`>>>> ${componentDetails}`)}`; 99 | } else { 100 | formattedMessage = `${logEmoji} [${formattedTimestamp}] ${levelLog}: ${formattedMessage}`; 101 | } 102 | 103 | return formattedMessage; 104 | }), 105 | ), 106 | transports: [new winston.transports.Console({ level: 'debug' })], 107 | }); 108 | 109 | export { logger, inAppLogger }; 110 | -------------------------------------------------------------------------------- /tests/try_catch.test.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { DRequest, DResponse, DNextFunc } from '../common'; 3 | import { TryCatchAsyncDec, TryCatchAsyncFn, TryCatchDec, TryCatchFn } from '../common'; 4 | 5 | describe('TryCatch Utilities', () => { 6 | let req: Partial; 7 | let res: Partial; 8 | let next: DNextFunc; 9 | 10 | beforeEach(() => { 11 | req = {}; 12 | res = { 13 | status: jest.fn().mockReturnThis(), 14 | json: jest.fn(), 15 | }; 16 | next = jest.fn(); 17 | }); 18 | 19 | describe('TryCatchAsyncFn', () => { 20 | it('should call the next function with an error if the function throws', async () => { 21 | const error = new Error('Test error'); 22 | const asyncFunction = TryCatchAsyncFn(async (req, res, next) => { 23 | throw error; 24 | }); 25 | 26 | await asyncFunction(req as DRequest, res as DResponse, next); 27 | 28 | expect(next).toHaveBeenCalledWith(error); 29 | }); 30 | 31 | it('should call the original function if no error is thrown', async () => { 32 | const asyncFunction = TryCatchAsyncFn(async (req, res, next) => { 33 | res.status(200).json({ message: 'Success' }); 34 | }); 35 | 36 | await asyncFunction(req as DRequest, res as DResponse, next); 37 | 38 | expect(res.status).toHaveBeenCalledWith(200); 39 | expect(res.json).toHaveBeenCalledWith({ message: 'Success' }); 40 | expect(next).not.toHaveBeenCalled(); 41 | }); 42 | }); 43 | 44 | describe('TryCatchFn', () => { 45 | // it('should call the next function with an error if the function throws', () => { 46 | // const error = new Error('Test error'); 47 | // const syncFunction = TryCatchFn((req, res, next) => { 48 | // throw error; 49 | // }); 50 | 51 | // syncFunction(req as DRequest, res as DResponse, next); 52 | 53 | // expect(next).toHaveBeenCalledWith(error); 54 | // }); 55 | 56 | it('should call the original function if no error is thrown', () => { 57 | const syncFunction = TryCatchFn((req, res, next) => { 58 | res.status(200).json({ message: 'Success' }); 59 | }); 60 | 61 | syncFunction(req as DRequest, res as DResponse, next); 62 | 63 | expect(res.status).toHaveBeenCalledWith(200); 64 | expect(res.json).toHaveBeenCalledWith({ message: 'Success' }); 65 | expect(next).not.toHaveBeenCalled(); 66 | }); 67 | }); 68 | 69 | describe('TryCatchAsyncDec', () => { 70 | it('should call the next function with an error if the method throws', async () => { 71 | const error = new Error('Test error'); 72 | class TestClass { 73 | @TryCatchAsyncDec 74 | async method(req: DRequest, res: DResponse, next: DNextFunc) { 75 | throw error; 76 | } 77 | } 78 | 79 | const instance = new TestClass(); 80 | await instance.method(req as DRequest, res as DResponse, next); 81 | 82 | expect(next).toHaveBeenCalledWith(error); 83 | }); 84 | 85 | it('should call the original method if no error is thrown', async () => { 86 | class TestClass { 87 | @TryCatchAsyncDec 88 | async method(req: DRequest, res: DResponse, next: DNextFunc) { 89 | res.status(200).json({ message: 'Success' }); 90 | } 91 | } 92 | 93 | const instance = new TestClass(); 94 | await instance.method(req as DRequest, res as DResponse, next); 95 | 96 | expect(res.status).toHaveBeenCalledWith(200); 97 | expect(res.json).toHaveBeenCalledWith({ message: 'Success' }); 98 | expect(next).not.toHaveBeenCalled(); 99 | }); 100 | }); 101 | 102 | describe('TryCatchDec', () => { 103 | it('should call the next function with an error if the method throws', () => { 104 | const error = new Error('Test error'); 105 | class TestClass { 106 | @TryCatchDec 107 | method(req: DRequest, res: DResponse, next: DNextFunc) { 108 | throw error; 109 | } 110 | } 111 | 112 | const instance = new TestClass(); 113 | instance.method(req as DRequest, res as DResponse, next); 114 | 115 | expect(next).toHaveBeenCalledWith(error); 116 | }); 117 | 118 | it('should call the original method if no error is thrown', () => { 119 | class TestClass { 120 | @TryCatchDec 121 | method(req: DRequest, res: DResponse, next: DNextFunc) { 122 | res.status(200).json({ message: 'Success' }); 123 | } 124 | } 125 | 126 | const instance = new TestClass(); 127 | instance.method(req as DRequest, res as DResponse, next); 128 | 129 | expect(res.status).toHaveBeenCalledWith(200); 130 | expect(res.json).toHaveBeenCalledWith({ message: 'Success' }); 131 | expect(next).not.toHaveBeenCalled(); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dolphjs/dolph", 3 | "version": "1.5.3", 4 | "description": "New-Age, developer friendly node.js and bun framework", 5 | "repository": "https://github.com/dolphjs/dolph", 6 | "author": "Utee Akaninyene", 7 | "license": "MIT", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "scripts": { 12 | "build:shell": "./compile.sh", 13 | "build:classes": "tsc -b classes", 14 | "build:common": "tsc -b common", 15 | "build:core": "tsc -b core", 16 | "build:decorators": "tsc -b decorators", 17 | "build:packages": "tsc -b packages", 18 | "build:utilities": "tsc -b utilities", 19 | "build:all": "npm run build:classes && npm run build:common && npm run build:core && npm run build:decorators && npm run build:packages && npm run build:utilities", 20 | "build": "tsc -b -v .", 21 | "build:dev": "tsc -b -v . --watch", 22 | "prebuild:prod": "npm run clean", 23 | "build:prod": "tsc -b -v common", 24 | "build:sample": "tsc -b -v samples", 25 | "test": "jest --forceExit --detectOpenHandles", 26 | "clean": "npm run clean:all", 27 | "clean:all": "tsc -b --clean classes common core decorators packages utilities", 28 | "format": "prettier \"**/*.ts\" \"common/**/*.json\" --ignore-path ./.prettierignore --write && git status", 29 | "copy:package-json": "gulp copy:package-json", 30 | "copy:readme": "gulp copy:readme", 31 | "copy:license": "gulp copy:license", 32 | "copy:npm-ignore": "gulp copy:npm-ignore", 33 | "copy:all": "npm run copy:package-json && npm run copy:readme && npm run copy:license && npm run copy:npm-ignore", 34 | "pre:release": "npm run test && npm run build && npm run copy:all" 35 | }, 36 | "lint-staged": { 37 | "**/*.ts": [ 38 | "prettier --ignore-path ./.prettierignore --write" 39 | ], 40 | "core/**/*.json": [ 41 | "prettier --ignore-path ./.prettierignore --write" 42 | ], 43 | "common/**/*.json": [ 44 | "prettier --ignore-path ./.prettierignore --write" 45 | ], 46 | "classes/**/*.json": [ 47 | "prettier --ignore-path ./.prettierignore --write" 48 | ], 49 | "decorators/**/*.json": [ 50 | "prettier --ignore-path ./.prettierignore --write" 51 | ], 52 | "packages/**/*.json": [ 53 | "prettier --ignore-path ./.prettierignore --write" 54 | ], 55 | "utilities/**/*.json": [ 56 | "prettier --ignore-path ./.prettierignore --write" 57 | ] 58 | }, 59 | "engines": { 60 | "node": ">= 18" 61 | }, 62 | "devDependencies": { 63 | "@dolphjs/graphql": "^0.2.1", 64 | "@types/autocannon": "^7.12.5", 65 | "@types/bcryptjs": "^2.4.2", 66 | "@types/busboy": "^1.5.4", 67 | "@types/cli-color": "^2.0.2", 68 | "@types/cookie-parser": "^1.4.6", 69 | "@types/cors": "^2.8.13", 70 | "@types/ejs": "^3.1.5", 71 | "@types/express": "^4.17.17", 72 | "@types/express-handlebars": "^6.0.0", 73 | "@types/gulp": "^4.0.13", 74 | "@types/jest": "^29.5.12", 75 | "@types/js-yaml": "^4.0.5", 76 | "@types/jsonwebtoken": "^9.0.2", 77 | "@types/mime-types": "^2.1.4", 78 | "@types/multer": "^1.4.12", 79 | "@types/node": "^20.3.2", 80 | "@types/pug": "^2.0.10", 81 | "@types/sequelize": "^4.28.15", 82 | "@types/socket.io": "^3.0.2", 83 | "@typescript-eslint/eslint-plugin": "^5.60.1", 84 | "@typescript-eslint/parser": "^5.60.1", 85 | "autocannon": "^7.15.0", 86 | "delete-empty": "^3.0.0", 87 | "ejs": "^3.1.10", 88 | "eslint": "^8.43.0", 89 | "eslint-config-prettier": "^8.8.0", 90 | "eslint-plugin-import": "^2.27.5", 91 | "form-data": "^4.0.1", 92 | "fs-temp": "^2.0.1", 93 | "graphql-scalars": "^1.23.0", 94 | "gulp": "^4.0.2", 95 | "gulp-clang-format": "^1.0.27", 96 | "gulp-clean": "^0.4.0", 97 | "gulp-sourcemaps": "^3.0.0", 98 | "gulp-typescript": "^6.0.0-alpha.1", 99 | "gulp-watch": "^5.0.1", 100 | "jest": "^29.7.0", 101 | "nodemon": "^3.0.1", 102 | "pug": "^3.0.3", 103 | "rimraf": "^6.0.1", 104 | "supertest": "^7.0.0", 105 | "ts-jest": "^29.2.3", 106 | "ts-node": "^10.9.1", 107 | "type-graphql": "^2.0.0-rc.2", 108 | "typescript": "^5.1.5" 109 | }, 110 | "dependencies": { 111 | "bcryptjs": "^2.4.3", 112 | "busboy": "^1.6.0", 113 | "class-transformer": "^0.5.1", 114 | "class-validator": "^0.14.1", 115 | "cli-color": "^2.0.3", 116 | "concat-stream": "^2.0.0", 117 | "cookie-parser": "^1.4.6", 118 | "cors": "^2.8.5", 119 | "dotenv": "^16.3.1", 120 | "express": "^4.18.2", 121 | "express-graphql": "^0.12.0", 122 | "express-handlebars": "^7.1.3", 123 | "helmet": "^7.1.0", 124 | "http-status": "^1.7.3", 125 | "joi": "^17.9.2", 126 | "js-yaml": "^4.1.0", 127 | "jsonwebtoken": "^9.0.1", 128 | "mime-types": "^2.1.35", 129 | "mongoose": "^8.0.1", 130 | "morgan": "^1.10.0", 131 | "reflect-metadata": "^0.2.1", 132 | "sequelize": "^6.33.0", 133 | "socket.io": "^4.7.5", 134 | "typedi": "^0.10.0", 135 | "winston": "^3.9.0", 136 | "xss": "^1.0.14", 137 | "xtend": "^4.0.2" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/uploader/file_uploader.ts: -------------------------------------------------------------------------------- 1 | import concat_stream from 'concat-stream'; 2 | import { randomBytes } from 'crypto'; 3 | import { createWriteStream, mkdirSync, unlink } from 'fs'; 4 | import { tmpdir } from 'os'; 5 | import { join } from 'path'; 6 | import { 7 | DiskStorageOptions, 8 | FileInfo, 9 | Storage, 10 | UploadConfig, 11 | UploadOptionAndConfig, 12 | } from '../../common/types/dolph_uploader.type'; 13 | import { DRequest } from '../../common'; 14 | import { makeMiddleware } from './make_middleware'; 15 | 16 | /** 17 | * @version 2.0 18 | * 19 | * @author Utee 20 | */ 21 | class DiskStorage implements Storage { 22 | private getFilename: NonNullable; 23 | private getDestination: NonNullable; 24 | 25 | constructor(options: DiskStorageOptions = {}) { 26 | if (options.destination && typeof options.destination !== 'string') { 27 | throw new Error('Destination must be a string path'); 28 | } 29 | 30 | this.getFilename = options.filename || defaultGetFilename; 31 | 32 | if (typeof options.destination === 'string') { 33 | createDirectory(options.destination); 34 | this.getDestination = (_req, _file, cb) => cb(null, options.destination as string); 35 | } else { 36 | this.getDestination = options.destination || defaultGetDestination; 37 | } 38 | } 39 | 40 | handleFile(req: DRequest, file: FileInfo, callback: (error: Error | null, info?: Partial) => void): void { 41 | //@ts-expect-error 42 | this.getDestination(req, file, (err, destination) => { 43 | if (err) return callback(err); 44 | 45 | this.getFilename(req, file, (err, filename) => { 46 | if (err) return callback(err); 47 | 48 | const finalPath = join(destination, filename); 49 | const outStream = createWriteStream(finalPath); 50 | 51 | file.stream!.pipe(outStream); 52 | outStream.on('error', callback); 53 | outStream.on('finish', () => { 54 | callback(null, { 55 | destination, 56 | filename, 57 | path: finalPath, 58 | size: outStream.bytesWritten, 59 | }); 60 | }); 61 | }); 62 | }); 63 | } 64 | 65 | removeFile(req: DRequest, file: FileInfo, callback: (error: Error | null) => void): void { 66 | const path = file.path; 67 | delete file.destination; 68 | delete file.filename; 69 | delete file.path; 70 | unlink(path!, callback); 71 | } 72 | } 73 | 74 | // MemoryStorage Implementation 75 | class MemoryStorage implements Storage { 76 | handleFile(req: DRequest, file: FileInfo, callback: (error: Error | null, info?: Partial) => void): void { 77 | file.stream!.pipe( 78 | concat_stream({ encoding: 'buffer' }, (data: Buffer) => { 79 | callback(null, { 80 | buffer: data, 81 | size: data.length, 82 | }); 83 | }), 84 | ); 85 | } 86 | 87 | removeFile(req: DRequest, file: FileInfo, callback: (error: Error | null) => void): void { 88 | delete file.buffer; 89 | callback(null); 90 | } 91 | } 92 | 93 | // Helper functions 94 | function defaultGetFilename( 95 | _req: DRequest, 96 | _file: FileInfo, 97 | callback: (error: Error | null, filename: string) => void, 98 | ): void { 99 | randomBytes(16, (err, raw) => { 100 | callback(err, err ? '' : raw.toString('hex')); 101 | }); 102 | } 103 | 104 | function defaultGetDestination( 105 | _req: DRequest, 106 | _file: FileInfo, 107 | callback: (error: Error | null, destination: string) => void, 108 | ): void { 109 | callback(null, tmpdir()); 110 | } 111 | 112 | function createDirectory(destination: string): void { 113 | try { 114 | mkdirSync(destination, { recursive: true }); 115 | } catch (error: any) { 116 | if (error.code !== 'EEXIST') { 117 | throw error; 118 | } 119 | } 120 | } 121 | 122 | // Middleware creator function 123 | function createUploadMiddleware(options: Partial = {}) { 124 | if (!options.fieldname) throw new Error('Provide the `fieldname` option'); 125 | 126 | if (options.type == 'single') { 127 | return makeMiddleware( 128 | () => 129 | ({ 130 | ...options, 131 | fileStrategy: 'VALUE', 132 | fields: [{ name: options.fieldname, maxCount: 1 }], 133 | }) as UploadConfig, 134 | ); 135 | } else if (options.type == 'array') { 136 | if (!options.maxCount) throw new Error('Provide the `maxCount` option'); 137 | return makeMiddleware( 138 | () => 139 | ({ 140 | ...options, 141 | fileStrategy: 'ARRAY', 142 | fields: [{ name: options.fieldname, maxCount: options.maxCount }], 143 | }) as UploadConfig, 144 | ); 145 | } else if (options.type == 'fields') { 146 | if (!options.fields) throw new Error('Provide the `field` option'); 147 | return makeMiddleware( 148 | () => 149 | ({ 150 | ...options, 151 | fileStrategy: 'OBJECT', 152 | fields: options.fields, 153 | }) as UploadConfig, 154 | ); 155 | } else { 156 | throw new Error('Provide a valid and supported type'); 157 | } 158 | } 159 | 160 | export const fileUploader = createUploadMiddleware; 161 | export const diskStorage = (options: DiskStorageOptions) => new DiskStorage(options); 162 | export const memoryStorage = () => new MemoryStorage(); 163 | -------------------------------------------------------------------------------- /classes/jwt_auth_classes.class.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorizationFunction, 3 | DNextFunc, 4 | DRequest, 5 | DResponse, 6 | ErrorException, 7 | HttpStatus, 8 | IPayload, 9 | TryCatchAsyncFn, 10 | } from '../common'; 11 | import { verifyJWTwithHMAC, verifyJWTwithRSA } from '../utilities'; 12 | import { config } from 'dotenv'; 13 | config(); 14 | 15 | const authHeaderName: Array = ['x-auth-token', 'authorization']; 16 | 17 | /** 18 | * Takes a `tokenSecret` parameter 19 | * 20 | * The Verify method is used to verify JWT token/ private key 21 | */ 22 | // Authorization - used when it's just secret used 23 | // x-auth-token - used when it's private and public keys being used 24 | class JwtBasicAuth { 25 | tokenSecret: string; 26 | constructor(tokenSecret: string) { 27 | this.tokenSecret = tokenSecret; 28 | } 29 | 30 | Verify = TryCatchAsyncFn(async (req: DRequest, res: DResponse, next: DNextFunc) => { 31 | let authToken: string | string[]; 32 | let authHeader: string; 33 | authHeaderName.forEach((headerName) => { 34 | if (req.headers[headerName]) { 35 | authToken = req.headers[headerName]; 36 | authHeader = headerName; 37 | } 38 | }); 39 | if (authToken === '' || !authToken?.length) { 40 | return next(new ErrorException('provide a valid token header', HttpStatus.UNAUTHORIZED)); 41 | } 42 | let payload: IPayload; 43 | if (authHeader === 'Authorization') { 44 | //@ts-expect-error 45 | payload = verifyJWTwithHMAC({ token: authToken, secret: this.tokenSecret }); 46 | } else if (authHeader === 'x-auth-token') { 47 | //@ts-expect-error 48 | payload = verifyJWTwithRSA({ pathToPublicKey: this.tokenSecret, token: authToken }); 49 | } 50 | 51 | return payload; 52 | }); 53 | } 54 | 55 | /** 56 | * 57 | * class-method decorator used for authorization based on the dolphjs default JWT authentication and authorization design 58 | * 59 | * @version 1.0.4 60 | */ 61 | const JWTAuthVerifyDec = (tokenSecret: string) => { 62 | return (_target: any, _propertyKey: string, descriptor?: TypedPropertyDescriptor) => { 63 | const originalMethod = descriptor.value; 64 | 65 | // convert to normal func 66 | descriptor.value = (req: DRequest, res: DResponse, next: DNextFunc) => { 67 | try { 68 | const context = this; 69 | let authToken: string | string[]; 70 | let authHeader: string; 71 | 72 | if (req.headers[authHeaderName[0]]) { 73 | authToken = req.headers[authHeaderName[0]]; 74 | authHeader = authHeaderName[0]; 75 | } else if (req.headers[authHeaderName[1]]) { 76 | authToken = req.headers[authHeaderName[1]]; 77 | authHeader = authHeaderName[1]; 78 | } 79 | 80 | if (authToken === '' || !authToken?.length) 81 | return next(new ErrorException('provide a valid token header', HttpStatus.UNAUTHORIZED)); 82 | 83 | let payload: IPayload; 84 | if (authHeader === authHeaderName[1]) { 85 | //@ts-expect-error 86 | payload = verifyJWTwithHMAC({ token: authToken, secret: tokenSecret }); 87 | } else if (authHeader === authHeaderName[0]) { 88 | //@ts-expect-error 89 | payload = verifyJWTwithRSA({ pathToPublicKey: tokenSecret, token: authToken }); 90 | } 91 | req.payload = payload; 92 | 93 | return originalMethod.apply(context, [req, res, next]); 94 | } catch (e) { 95 | throw e; 96 | } 97 | }; 98 | 99 | // return descriptor; 100 | }; 101 | }; 102 | 103 | const JWTAuthorizeDec = (tokenSecret: string, authorize?: AuthorizationFunction) => { 104 | return (_target: any, _propertyKey: string, descriptor?: TypedPropertyDescriptor) => { 105 | const originalMethod = descriptor.value; 106 | 107 | descriptor.value = async (req: DRequest, res: DResponse, next: DNextFunc) => { 108 | try { 109 | const context = this; 110 | let authToken: string | string[]; 111 | let authHeader: string; 112 | 113 | if (req.headers[authHeaderName[0]]) { 114 | authToken = req.headers[authHeaderName[0]]; 115 | authHeader = authHeaderName[0]; 116 | } else if (req.headers[authHeaderName[1]]) { 117 | authToken = req.headers[authHeaderName[1]]; 118 | authHeader = authHeaderName[1]; 119 | } 120 | 121 | if (authToken === '' || !authToken?.length) 122 | return next(new ErrorException('provide a valid token header', HttpStatus.UNAUTHORIZED)); 123 | 124 | let payload: IPayload; 125 | if (authHeader === authHeaderName[1]) { 126 | //@ts-expect-error 127 | payload = verifyJWTwithHMAC({ token: authToken, secret: tokenSecret }); 128 | } else if (authHeader === authHeaderName[0]) { 129 | //@ts-expect-error 130 | payload = verifyJWTwithRSA({ pathToPublicKey: tokenSecret, token: authToken }); 131 | } 132 | req.payload = payload; 133 | 134 | // Check if authorization function is provided 135 | if (authorize) { 136 | const isAuthorized = await authorize(payload); 137 | if (!isAuthorized) { 138 | return next(new ErrorException('Access denied', HttpStatus.FORBIDDEN)); 139 | } 140 | } 141 | 142 | return originalMethod.apply(context, [req, res, next]); 143 | } catch (e) { 144 | throw e; 145 | } 146 | }; 147 | }; 148 | }; 149 | 150 | export { JwtBasicAuth, JWTAuthVerifyDec, JWTAuthorizeDec }; 151 | -------------------------------------------------------------------------------- /tests/uploader/fileuploader.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { diskStorage, fileUploader } from '../../packages'; 3 | import { DRequest, DResponse } from '../../common'; 4 | import { Readable } from 'stream'; 5 | 6 | describe('File Uploader Test', () => { 7 | const mockNext = jest.fn(); 8 | let upload: ReturnType; 9 | 10 | beforeEach(() => { 11 | upload = fileUploader({ 12 | storage: diskStorage({ destination: join(__dirname, 'uploads') }), 13 | type: 'single', // Add this to match current fileUploader implementation 14 | fieldname: 'avatar', // Add a default fieldname 15 | }); 16 | mockNext.mockClear(); 17 | }); 18 | 19 | test('single file upload should work correctly', (done) => { 20 | const middleware = upload; 21 | 22 | const mockFileStream = new Readable(); 23 | mockFileStream.push('test file content'); 24 | mockFileStream.push(null); 25 | 26 | const mockRequest = { 27 | headers: { 28 | 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW', 29 | }, 30 | on: jest.fn((event, handler) => { 31 | if (event === 'file') { 32 | handler('avatar', { 33 | stream: mockFileStream, 34 | filename: 'test.txt', 35 | encoding: '7bit', 36 | mimetype: 'text/plain', 37 | }); 38 | } 39 | if (event === 'finish') { 40 | handler(); 41 | } 42 | return mockRequest; 43 | }), 44 | pipe: jest.fn(), 45 | } as unknown as DRequest; 46 | 47 | const mockResponse = {} as DResponse; 48 | 49 | middleware(mockRequest, mockResponse, (error) => { 50 | try { 51 | expect(error).toBeUndefined(); 52 | expect(mockRequest.file).toBeDefined(); 53 | done(); 54 | } catch (err) { 55 | done(err); 56 | } 57 | }); 58 | 59 | mockRequest.on('finish', () => {}); 60 | }, 15000); 61 | 62 | test('array upload should handle multiple files', (done) => { 63 | const upload = fileUploader({ 64 | type: 'array', 65 | fieldname: 'photos', 66 | maxCount: 3, 67 | storage: diskStorage({ destination: join(__dirname, 'uploads') }), 68 | }); 69 | 70 | const middleware = upload; 71 | 72 | const mockFileStream = new Readable(); 73 | mockFileStream.push('test file content'); 74 | mockFileStream.push(null); 75 | 76 | const mockRequest = { 77 | headers: { 78 | 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW', 79 | }, 80 | on: jest.fn((event, handler) => { 81 | if (event === 'file') { 82 | // Simulate multiple files 83 | handler('photos', { 84 | stream: mockFileStream, 85 | filename: 'test1.txt', 86 | encoding: '7bit', 87 | mimetype: 'text/plain', 88 | }); 89 | handler('photos', { 90 | stream: mockFileStream, 91 | filename: 'test2.txt', 92 | encoding: '7bit', 93 | mimetype: 'text/plain', 94 | }); 95 | } 96 | if (event === 'finish') { 97 | handler(); 98 | } 99 | return mockRequest; 100 | }), 101 | pipe: jest.fn(), 102 | } as unknown as DRequest; 103 | 104 | const mockResponse = {} as DResponse; 105 | 106 | middleware(mockRequest, mockResponse, (error) => { 107 | try { 108 | expect(error).toBeUndefined(); 109 | expect(Array.isArray(mockRequest.files)).toBeTruthy(); 110 | done(); 111 | } catch (err) { 112 | done(err); 113 | } 114 | }); 115 | 116 | mockRequest.on('finish', () => {}); 117 | }, 15000); 118 | 119 | test('should handle non-multipart requests', (done) => { 120 | const middleware = upload; 121 | 122 | const mockRequest = { 123 | headers: { 124 | 'content-type': 'application/json', 125 | }, 126 | } as unknown as DRequest; 127 | 128 | const mockResponse = {} as DResponse; 129 | 130 | middleware(mockRequest, mockResponse, (error) => { 131 | expect(error).toBeUndefined(); 132 | expect(mockRequest.file).toBeUndefined(); 133 | done(); 134 | }); 135 | }); 136 | 137 | test('fields upload should handle multiple fields', (done) => { 138 | const upload = fileUploader({ 139 | type: 'fields', 140 | fields: [ 141 | { name: 'avatar', maxCount: 1 }, 142 | { name: 'gallery', maxCount: 3 }, 143 | ], 144 | fieldname: 'upload', 145 | storage: diskStorage({ destination: join(__dirname, 'uploads') }), 146 | }); 147 | 148 | const middleware = upload; 149 | 150 | const mockFileStream = new Readable(); 151 | mockFileStream.push('test file content'); 152 | mockFileStream.push(null); 153 | 154 | const mockRequest = { 155 | headers: { 156 | 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW', 157 | }, 158 | on: jest.fn((event, handler) => { 159 | if (event === 'file') { 160 | // Simulate files for different fields 161 | handler('avatar', { 162 | stream: mockFileStream, 163 | filename: 'avatar.jpg', 164 | encoding: '7bit', 165 | mimetype: 'image/jpeg', 166 | }); 167 | handler('gallery', { 168 | stream: mockFileStream, 169 | filename: 'gallery1.jpg', 170 | encoding: '7bit', 171 | mimetype: 'image/jpeg', 172 | }); 173 | } 174 | if (event === 'finish') { 175 | handler(); 176 | } 177 | return mockRequest; 178 | }), 179 | pipe: jest.fn(), 180 | } as unknown as DRequest; 181 | 182 | const mockResponse = {} as DResponse; 183 | 184 | middleware(mockRequest, mockResponse, (error) => { 185 | try { 186 | expect(error).toBeUndefined(); 187 | expect(mockRequest.files).toBeDefined(); 188 | done(); 189 | } catch (err) { 190 | done(err); 191 | } 192 | }); 193 | 194 | mockRequest.on('finish', () => {}); 195 | }, 1500); 196 | }); 197 | -------------------------------------------------------------------------------- /packages/uploader/make_middleware.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import BBusboy, { Busboy } from 'busboy'; 3 | import { FileInfo, UploadConfig } from '../../common/types/dolph_uploader.type'; 4 | import { DRequest, DResponse } from '../../common'; 5 | import { FileAppender } from './file_appender'; 6 | import { Counter } from './counter'; 7 | import { DolphFIleUploaderError } from './errors/error_messages'; 8 | import { removeUploadedFiles } from './remove_uploaded_files'; 9 | 10 | export function isMultipart(req: IncomingMessage): boolean { 11 | const contentType = req.headers['content-type'] || ''; 12 | return contentType.startsWith('multipart/'); 13 | } 14 | 15 | function onRequestFinished(req: IncomingMessage, callback: () => void): void { 16 | req.on('end', callback); 17 | req.on('close', callback); 18 | } 19 | 20 | function appendField(target: Record, key: string, value: any): void { 21 | if (Object.prototype.hasOwnProperty.call(target, key)) { 22 | if (!Array.isArray(target[key])) { 23 | target[key] = [target[key]]; 24 | } 25 | target[key].push(value); 26 | } else { 27 | target[key] = value; 28 | } 29 | } 30 | 31 | function drainStream(stream: NodeJS.ReadableStream): void { 32 | stream.on('readable', stream.read.bind(stream)); 33 | } 34 | 35 | export function makeMiddleware(getOptions: () => UploadConfig & { fields?: Array<{ name: string; maxCount?: number }> }) { 36 | return function uploadMiddleware(req: DRequest, res: DResponse, next: (error?: any) => void): void { 37 | if (!isMultipart(req)) { 38 | return next(); 39 | } 40 | 41 | let options: ReturnType; 42 | 43 | try { 44 | options = getOptions(); 45 | } catch (error) { 46 | return next(error); 47 | } 48 | 49 | req.body = Object.create(null); 50 | 51 | let busboy: Busboy; 52 | try { 53 | busboy = BBusboy({ 54 | headers: req.headers, 55 | limits: options.limits, 56 | preservePath: options.preservePaths, 57 | }); 58 | } catch (error) { 59 | return next(error); 60 | } 61 | 62 | const appender = new FileAppender(options.fileStrategy, req); 63 | const pendingWrites = new Counter(); 64 | const uploadedFiles: FileInfo[] = []; 65 | 66 | let isDone = false; 67 | let readFinished = false; 68 | let errorOccurred = false; 69 | 70 | let uploadTimeOut: NodeJS.Timeout; 71 | 72 | function done(err?: any): void { 73 | if (isDone) return; 74 | isDone = true; 75 | 76 | // clear all pending timeout 77 | if (uploadTimeOut) clearTimeout(uploadTimeOut); 78 | 79 | req.unpipe(busboy); 80 | drainStream(req); 81 | busboy.removeAllListeners(); 82 | 83 | // onRequestFinished(req, () => next(err)); 84 | 85 | // Force resolution with a setImmediate to ensure all callbacks are processed 86 | setImmediate(() => { 87 | next(err); 88 | }); 89 | } 90 | 91 | /** 92 | * Todo: allow developer to set the modify the timeout from outside. 93 | */ 94 | uploadTimeOut = setTimeout(() => { 95 | console.error('Upload middleware timed out'); 96 | done(new Error('Upload process timed out')); 97 | }, 3000); 98 | 99 | function indicateDone(): void { 100 | if (readFinished && pendingWrites.isZero() && !errorOccurred) { 101 | done(); 102 | } 103 | } 104 | 105 | function abortWithError(uploaderError: Error): void { 106 | if (errorOccurred) return; 107 | errorOccurred = true; 108 | 109 | pendingWrites.onceZero(() => { 110 | removeUploadedFiles( 111 | uploadedFiles, 112 | (file: FileInfo, cb: (error: Error | null) => void) => options.storage.removeFile(req, file, cb), 113 | (err: Error | null, storageErrors: Error[]) => { 114 | if (err) return done(err); 115 | (uploaderError as any).storageErrors = storageErrors; 116 | done(uploaderError); 117 | }, 118 | ); 119 | }); 120 | } 121 | 122 | function abortWithCode(code: string, field?: string): void { 123 | abortWithError(new DolphFIleUploaderError(code, field)); 124 | } 125 | 126 | busboy.on('field', (fieldname: string, value: any, fieldNameTruncated: boolean, valueTruncated: boolean) => { 127 | if (!fieldname) return abortWithCode('MISSING_FIELD_NAME'); 128 | if (fieldNameTruncated) return abortWithCode('LIMIT_FIELD_KEY'); 129 | if (valueTruncated) return abortWithCode('LIMIT_FIELD_VALUE', fieldname); 130 | 131 | if (options.limits?.fieldNameSize && fieldname.length > options.limits.fieldNameSize) { 132 | return abortWithCode('LIMIT_FIELD_KEY'); 133 | } 134 | 135 | appendField(req.body!, fieldname, value); 136 | }); 137 | 138 | busboy.on( 139 | 'file', 140 | (fieldname: string, fileStream: NodeJS.ReadableStream, filename: any, encoding: string, mimetype: string) => { 141 | if (!fieldname) return fileStream.resume(); 142 | 143 | if (options.limits?.fieldNameSize && fieldname.length > options.limits.fieldNameSize) { 144 | return abortWithCode('LIMIT_FIELD_KEY'); 145 | } 146 | 147 | const file: FileInfo = { 148 | fieldname, 149 | originalname: filename.filename || filename, 150 | encoding: filename.encoding || encoding || '7bit', 151 | mimetype: filename.mimeType || mimetype || 'application/octet-stream', 152 | }; 153 | 154 | const placeholder = appender.insertPlaceholder(file); 155 | 156 | // if (!options.fileFilter) { 157 | // // options.fileFilter = (req, file) => (err: Error | null, includeFile: boolean) => { 158 | // // if (err || !includeFile) { 159 | // // appender.removePlaceholder(placeholder); 160 | // // return fileStream.resume(); 161 | // // } 162 | 163 | // // let aborting = false; 164 | // // pendingWrites.increment(); 165 | 166 | // // Object.defineProperty(file, 'stream', { 167 | // // configurable: true, 168 | // // enumerable: false, 169 | // // value: fileStream, 170 | // // }); 171 | 172 | // // fileStream.on('error', (err: Error) => { 173 | // // pendingWrites.decrement(); 174 | // // abortWithError(err); 175 | // // }); 176 | 177 | // // fileStream.on('limit', () => { 178 | // // aborting = true; 179 | // // abortWithCode('LIMIT_FILE_SIZE', fieldname); 180 | // // }); 181 | 182 | // // options.storage.handleFile(req, file, (err: Error | null, info?: Partial) => { 183 | // // if (aborting) { 184 | // // appender.removePlaceholder(placeholder); 185 | // // uploadedFiles.push({ ...file, ...info }); 186 | // // return pendingWrites.decrement(); 187 | // // } 188 | 189 | // // if (err) { 190 | // // appender.removePlaceholder(placeholder); 191 | // // pendingWrites.decrement(); 192 | // // return abortWithError(err); 193 | // // } 194 | 195 | // // const fileInfo = { ...file, ...info }; 196 | // // appender.replacePlaceholder(placeholder, fileInfo); 197 | // // uploadedFiles.push(fileInfo); 198 | // // pendingWrites.decrement(); 199 | // // indicateDone(); 200 | // // }); 201 | // // }; 202 | // options.fileFilter = (_req, file, callback) => { 203 | // callback(null, true); 204 | // }; 205 | // } else { 206 | // options.fileFilter(req, file, (err: Error | null, includeFile: boolean) => { 207 | // if (err || !includeFile) { 208 | // appender.removePlaceholder(placeholder); 209 | // return fileStream.resume(); 210 | // } 211 | 212 | // let aborting = false; 213 | // pendingWrites.increment(); 214 | 215 | // Object.defineProperty(file, 'stream', { 216 | // configurable: true, 217 | // enumerable: false, 218 | // value: fileStream, 219 | // }); 220 | 221 | // fileStream.on('error', (err: Error) => { 222 | // pendingWrites.decrement(); 223 | // abortWithError(err); 224 | // }); 225 | 226 | // fileStream.on('limit', () => { 227 | // aborting = true; 228 | // abortWithCode('LIMIT_FILE_SIZE', fieldname); 229 | // }); 230 | 231 | // options.storage.handleFile(req, file, (err: Error | null, info?: Partial) => { 232 | // if (aborting) { 233 | // appender.removePlaceholder(placeholder); 234 | // uploadedFiles.push({ ...file, ...info }); 235 | // return pendingWrites.decrement(); 236 | // } 237 | 238 | // if (err) { 239 | // appender.removePlaceholder(placeholder); 240 | // pendingWrites.decrement(); 241 | // return abortWithError(err); 242 | // } 243 | 244 | // const fileInfo = { ...file, ...info }; 245 | // appender.replacePlaceholder(placeholder, fileInfo); 246 | // uploadedFiles.push(fileInfo); 247 | // pendingWrites.decrement(); 248 | // indicateDone(); 249 | // }); 250 | // }); 251 | // } 252 | 253 | options.fileFilter(req, file, (err: Error | null, includeFile: boolean) => { 254 | if (err || !includeFile) { 255 | appender.removePlaceholder(placeholder); 256 | return fileStream.resume(); 257 | } 258 | 259 | let aborting = false; 260 | pendingWrites.increment(); 261 | 262 | Object.defineProperty(file, 'stream', { 263 | configurable: true, 264 | enumerable: false, 265 | value: fileStream, 266 | }); 267 | 268 | fileStream.on('error', (streamErr: Error) => { 269 | pendingWrites.decrement(); 270 | abortWithError(streamErr); 271 | }); 272 | 273 | fileStream.on('limit', () => { 274 | aborting = true; 275 | abortWithCode('LIMIT_FILE_SIZE', fieldname); 276 | }); 277 | 278 | options.storage.handleFile(req, file, (storageErr: Error | null, info?: Partial) => { 279 | if (aborting) { 280 | appender.removePlaceholder(placeholder); 281 | uploadedFiles.push({ ...file, ...info }); 282 | return pendingWrites.decrement(); 283 | } 284 | 285 | if (storageErr) { 286 | appender.removePlaceholder(placeholder); 287 | pendingWrites.decrement(); 288 | return abortWithError(storageErr); 289 | } 290 | 291 | const fileInfo = { ...file, ...info }; 292 | appender.replacePlaceholder(placeholder, fileInfo); 293 | uploadedFiles.push(fileInfo); 294 | pendingWrites.decrement(); 295 | indicateDone(); 296 | }); 297 | }); 298 | }, 299 | ); 300 | 301 | busboy.on('error', (err: Error) => abortWithError(err)); 302 | busboy.on('partsLimit', () => abortWithCode('LIMIT_PART_COUNT')); 303 | busboy.on('filesLimit', () => abortWithCode('LIMIT_FILE_COUNT')); 304 | busboy.on('fieldsLimit', () => abortWithCode('LIMIT_FIELD_COUNT')); 305 | busboy.on('finish', () => { 306 | readFinished = true; 307 | // this ensures that indicateDone is called in next tick 308 | setImmediate(indicateDone); 309 | // indicateDone(); 310 | }); 311 | 312 | req.pipe(busboy); 313 | }; 314 | } 315 | --------------------------------------------------------------------------------