├── .prettierignore ├── spec ├── views │ ├── index.hbs │ ├── partials │ │ └── unusedpartial.hbs │ └── layouts │ │ └── main.hbs ├── static │ └── test.txt ├── base-path.test.ts ├── cors.test.ts ├── response-decorator.test.ts ├── view-renderer.test.ts ├── render-static-files.test.ts ├── sse.test.ts ├── queue.test.ts ├── websocket │ ├── method-param-decorator.test.ts │ ├── auth-guard.test.ts │ ├── exception-filter.test.ts │ └── middleware.test.ts ├── lifecycle-hook.test.ts ├── schedule.test.ts ├── http-methods.test.ts ├── scoped-lifecycle-hook.test.ts ├── scoped-lifecycle-hook-another-order.test.ts ├── validate-body-decorator.test.ts ├── events.test.ts ├── exception-filter.test.ts ├── middleware.test.ts ├── auth-guard.test.ts └── injection.test.ts ├── mod.ts ├── tool └── hooks │ └── pre-commit ├── src ├── sse │ ├── mod.ts │ ├── message.ts │ └── event.ts ├── events │ ├── constants.ts │ ├── mod.ts │ ├── decorator.ts │ ├── module.ts │ └── events.ts ├── utils │ ├── mod.ts │ ├── constructor.ts │ ├── filepath.ts │ ├── serve-static.ts │ └── get-mime.ts ├── exception │ ├── mod.ts │ ├── http │ │ ├── mod.ts │ │ └── enum.ts │ └── filter │ │ ├── mod.ts │ │ ├── global-container.ts │ │ ├── interface.ts │ │ ├── decorator.ts │ │ └── executor.ts ├── router │ ├── controller │ │ ├── params │ │ │ ├── mod.ts │ │ │ ├── constants.ts │ │ │ ├── resolver.ts │ │ │ └── decorators.ts │ │ ├── mod.ts │ │ ├── constructor.ts │ │ └── decorator.ts │ ├── websocket │ │ ├── payload.ts │ │ ├── decorator.ts │ │ └── router.ts │ ├── middleware │ │ ├── mod.ts │ │ ├── global-container.ts │ │ ├── decorator.ts │ │ └── executor.ts │ ├── mod.ts │ ├── utils.ts │ └── router.ts ├── kv-queue │ ├── mod.ts │ ├── constants.ts │ ├── decorator.ts │ ├── kv.ts │ └── module.ts ├── module │ ├── mod.ts │ ├── constructor.ts │ └── decorator.ts ├── schedule │ ├── mod.ts │ ├── constants.ts │ ├── types.ts │ ├── decorator.ts │ ├── module.ts │ └── enum.ts ├── injector │ ├── injectable │ │ ├── mod.ts │ │ ├── helper.ts │ │ ├── constructor.ts │ │ └── decorator.ts │ ├── mod.ts │ └── decorator.ts ├── renderer │ ├── interface.ts │ ├── decorator.ts │ └── handlebar.ts ├── hook │ ├── mod.ts │ ├── interfaces.ts │ └── executor.ts ├── metadata │ ├── mod.ts │ ├── decorator.ts │ └── helper.ts ├── guard │ ├── mod.ts │ ├── constants.ts │ ├── interface.ts │ ├── decorator.ts │ └── executor.ts ├── deps_test.ts ├── mod.ts ├── deps.ts ├── logger.ts └── app.ts ├── .vscode └── settings.json ├── validation.ts ├── .gitignore ├── .github ├── workflows │ ├── main.yml │ └── run-tests.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── CONTRIBUTING.md ├── example ├── events.ts ├── schedule.ts └── run.ts ├── deno.json ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.prettierignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /spec/views/index.hbs: -------------------------------------------------------------------------------- 1 | {{title}} 2 | -------------------------------------------------------------------------------- /spec/views/partials/unusedpartial.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from './src/mod.ts'; 2 | -------------------------------------------------------------------------------- /spec/static/test.txt: -------------------------------------------------------------------------------- 1 | I love pikachu 2 | -------------------------------------------------------------------------------- /spec/views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | {{{body}}} 2 | -------------------------------------------------------------------------------- /tool/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | deno lint 3 | -------------------------------------------------------------------------------- /src/sse/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './event.ts'; 2 | export * from './message.ts'; -------------------------------------------------------------------------------- /src/events/constants.ts: -------------------------------------------------------------------------------- 1 | export const eventListenerMetadataKey = 'event-listener'; 2 | -------------------------------------------------------------------------------- /src/utils/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './constructor.ts'; 4 | -------------------------------------------------------------------------------- /src/exception/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './http/mod.ts'; 2 | export * from './filter/mod.ts'; 3 | -------------------------------------------------------------------------------- /src/router/controller/params/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './decorators.ts'; 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /src/kv-queue/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator.ts'; 2 | export * from './kv.ts'; 3 | export * from './module.ts'; 4 | -------------------------------------------------------------------------------- /src/events/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator.ts'; 2 | export * from './events.ts'; 3 | export * from './module.ts'; 4 | -------------------------------------------------------------------------------- /src/exception/http/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './enum.ts'; 4 | export * from './exceptions.ts'; 5 | -------------------------------------------------------------------------------- /src/module/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './constructor.ts'; 4 | export * from './decorator.ts'; 5 | -------------------------------------------------------------------------------- /src/router/controller/params/constants.ts: -------------------------------------------------------------------------------- 1 | export const argumentResolverFunctionsMetadataKey = 'argumentResolverFunctions'; -------------------------------------------------------------------------------- /src/schedule/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator.ts'; 2 | export * from './module.ts'; 3 | export * from './enum.ts'; 4 | -------------------------------------------------------------------------------- /src/router/websocket/payload.ts: -------------------------------------------------------------------------------- 1 | export interface WebSocketPayload { 2 | topic: string; 3 | data: T; 4 | } 5 | -------------------------------------------------------------------------------- /src/exception/filter/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator.ts'; 2 | export * from './executor.ts'; 3 | export * from './interface.ts'; 4 | -------------------------------------------------------------------------------- /src/injector/injectable/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './constructor.ts'; 4 | export * from './decorator.ts'; 5 | -------------------------------------------------------------------------------- /src/router/middleware/mod.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator.ts'; 2 | export * from './executor.ts'; 3 | export * from './global-container.ts'; 4 | -------------------------------------------------------------------------------- /src/sse/message.ts: -------------------------------------------------------------------------------- 1 | export interface SSEMessage { 2 | data: string | object; 3 | event?: string; 4 | id?: string; 5 | retry?: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/injector/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './decorator.ts'; 4 | export * from './injector.ts'; 5 | export * from './injectable/mod.ts'; 6 | -------------------------------------------------------------------------------- /validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Validation module. 4 | * Export everything from https://jsr.io/@danet/validatte 5 | */ 6 | export * from 'validatte'; 7 | -------------------------------------------------------------------------------- /src/renderer/interface.ts: -------------------------------------------------------------------------------- 1 | export interface Renderer { 2 | setRootDir(directory: string): void; 3 | render(filename: string, data: unknown): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/router/controller/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './constructor.ts'; 4 | export * from './decorator.ts'; 5 | export * from './params/mod.ts'; 6 | -------------------------------------------------------------------------------- /src/hook/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Export useful interfaces for lifecycle hooks. 4 | */ 5 | 6 | export * from './executor.ts'; 7 | export * from './interfaces.ts'; 8 | -------------------------------------------------------------------------------- /src/schedule/constants.ts: -------------------------------------------------------------------------------- 1 | export const scheduleMetadataKey = 'task-scheduler'; 2 | export const intervalMetadataKey = 'interval'; 3 | export const timeoutMetadataKey = 'timeout'; 4 | -------------------------------------------------------------------------------- /src/utils/constructor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic constructor type. 3 | */ 4 | // deno-lint-ignore no-explicit-any 5 | export type Constructor = new (...args: any[]) => T; 6 | -------------------------------------------------------------------------------- /src/metadata/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Provide helper function and decorators for metadata. 4 | */ 5 | 6 | export * from './helper.ts'; 7 | export * from './decorator.ts'; 8 | -------------------------------------------------------------------------------- /src/guard/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './constants.ts'; 4 | export * from './decorator.ts'; 5 | export * from './executor.ts'; 6 | export * from './interface.ts'; 7 | -------------------------------------------------------------------------------- /src/module/constructor.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../utils/constructor.ts'; 2 | 3 | /** 4 | * Type Alias for Constructor 5 | */ 6 | 7 | export type ModuleConstructor = Constructor; 8 | -------------------------------------------------------------------------------- /src/guard/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The global guard identifier. 3 | * This can be used to apply a guard globally across the application. 4 | */ 5 | export const GLOBAL_GUARD = 'global-guard'; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | out 8 | gen 9 | node_modules 10 | coverage 11 | deno.lock 12 | -------------------------------------------------------------------------------- /src/router/controller/constructor.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../../utils/constructor.ts'; 2 | 3 | /** 4 | * Type Alias for Constructor 5 | */ 6 | export type ControllerConstructor = Constructor; 7 | -------------------------------------------------------------------------------- /src/sse/event.ts: -------------------------------------------------------------------------------- 1 | import { SSEMessage } from './message.ts'; 2 | 3 | export class SSEEvent extends CustomEvent { 4 | constructor(message: SSEMessage) { 5 | super('message', { detail: message }); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/kv-queue/constants.ts: -------------------------------------------------------------------------------- 1 | export const queueListenerMetadataKey = 'queue-listener'; 2 | 3 | export const KV_QUEUE_NAME = 'KV_QUEUE_NAME'; 4 | 5 | // deno-lint-ignore no-explicit-any 6 | export type QueueEvent = { 7 | type: string; 8 | data: T; 9 | }; 10 | -------------------------------------------------------------------------------- /src/schedule/types.ts: -------------------------------------------------------------------------------- 1 | export type CronString = Parameters[1]; 2 | 3 | export type CronMetadataPayload = { cron: CronString }; 4 | export type IntervalMetadataPayload = { interval: number }; 5 | export type TimeoutMetadataPayload = { timeout: number }; 6 | -------------------------------------------------------------------------------- /src/router/mod.ts: -------------------------------------------------------------------------------- 1 | // created from ctix 2 | 3 | export * from './router.ts'; 4 | export * from './utils.ts'; 5 | export * from './controller/mod.ts'; 6 | export * from './middleware/mod.ts'; 7 | export * from './websocket/decorator.ts'; 8 | export * from './websocket/payload.ts'; 9 | -------------------------------------------------------------------------------- /src/router/utils.ts: -------------------------------------------------------------------------------- 1 | export function trimSlash(path: string): string { 2 | if (path[path.length - 1] === '/') { 3 | path = path.substring(0, path.length - 1); 4 | } 5 | if (path[0] === '/') { 6 | path = path.substring(1, path.length); 7 | } 8 | return path; 9 | } 10 | -------------------------------------------------------------------------------- /src/guard/interface.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '../router/router.ts'; 2 | 3 | /** 4 | * An authentication guard is responsible for determining whether a request 5 | * should be allowed to proceed based on the provided execution context. 6 | */ 7 | export interface AuthGuard { 8 | canActivate(context: ExecutionContext): Promise | boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/router/websocket/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataFunction, SetMetadata } from '../../metadata/decorator.ts'; 2 | 3 | export function WebSocketController(endpoint = ''): MetadataFunction { 4 | return SetMetadata('websocket-endpoint', endpoint); 5 | } 6 | 7 | export function OnWebSocketMessage(topic: string): MetadataFunction { 8 | return SetMetadata('websocket-topic', topic); 9 | } 10 | -------------------------------------------------------------------------------- /src/deps_test.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assertSpyCall, 3 | assertSpyCallArg, 4 | assertSpyCalls, 5 | spy, 6 | } from '@std/testing/mock'; 7 | export { FakeTime } from '@std/testing/time'; 8 | export { 9 | assertEquals, 10 | assertInstanceOf, 11 | assertNotEquals, 12 | assertObjectMatch, 13 | assertRejects, 14 | assertThrows, 15 | } from '@std/testing/asserts'; 16 | export * as path from '@std/path'; 17 | -------------------------------------------------------------------------------- /src/exception/filter/global-container.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter } from './interface.ts'; 2 | 3 | /** 4 | * A global container for global exception filters. 5 | * 6 | * This array holds instances of middleware that can be applied globally 7 | * across the application. Each element in the array should conform to the 8 | * `PossibleMiddlewareType` type. 9 | * 10 | * @type {ExceptionFilter[]} 11 | */ 12 | export const globalExceptionFilterContainer: ExceptionFilter[] = []; 13 | -------------------------------------------------------------------------------- /src/router/middleware/global-container.ts: -------------------------------------------------------------------------------- 1 | import { PossibleMiddlewareType } from './decorator.ts'; 2 | 3 | /** 4 | * A global container for middleware functions. 5 | * 6 | * This array holds instances of middleware that can be applied globally 7 | * across the application. Each element in the array should conform to the 8 | * `PossibleMiddlewareType` type. 9 | * 10 | * @type {PossibleMiddlewareType[]} 11 | */ 12 | export const globalMiddlewareContainer: PossibleMiddlewareType[] = []; 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: read 14 | id-token: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup Deno 20 | uses: denoland/setup-deno@v1 21 | with: 22 | deno-version: canary 23 | 24 | - name: Publish package 25 | run: deno publish -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * 4 | * A probably too big module that exports almost everything. 5 | */ 6 | 7 | export * from './app.ts'; 8 | export * from './utils/mod.ts'; 9 | export * from './exception/mod.ts'; 10 | export * from './router/mod.ts'; 11 | export * from './module/mod.ts'; 12 | export * from './injector/mod.ts'; 13 | export * from './guard/mod.ts'; 14 | export * from './logger.ts'; 15 | export * from './events/mod.ts'; 16 | export * from './schedule/mod.ts'; 17 | export * from './kv-queue/mod.ts'; 18 | export * from './sse/mod.ts'; -------------------------------------------------------------------------------- /src/exception/filter/interface.ts: -------------------------------------------------------------------------------- 1 | import { HttpContext } from '../../router/router.ts'; 2 | 3 | /** 4 | * Interface representing an exception filter. 5 | * 6 | * This interface defines a method to handle exceptions that occur within an HTTP context. 7 | * Implementations of this interface can provide custom logic for handling different types of exceptions. 8 | * 9 | * @interface ExceptionFilter 10 | */ 11 | export interface ExceptionFilter { 12 | catch( 13 | exception: unknown, 14 | context: HttpContext, 15 | ): undefined | Response | { topic: string; data: unknown }; 16 | } 17 | -------------------------------------------------------------------------------- /src/guard/decorator.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../utils/constructor.ts'; 2 | import { MetadataFunction, SetMetadata } from '../metadata/decorator.ts'; 3 | 4 | export const guardMetadataKey = 'authGuards'; 5 | /** 6 | * Applies a guard to a route handler or controller. 7 | * 8 | * https://danet.land/overview/guards.html 9 | * 10 | * @param guard - The constructor of the guard to be applied. 11 | * @returns A function that sets the metadata for the guard. 12 | */ 13 | export function UseGuard(guard: Constructor): MetadataFunction { 14 | return SetMetadata(guardMetadataKey, guard); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../metadata/helper.ts'; 2 | import { ControllerConstructor } from '../router/controller/constructor.ts'; 3 | 4 | export const rendererViewFile = 'rendererViewFile'; 5 | export const Render = (fileName: string) => 6 | ( 7 | // deno-lint-ignore ban-types 8 | target: ControllerConstructor | Object, 9 | propertyKey?: string | symbol, 10 | // deno-lint-ignore no-explicit-any 11 | descriptor?: TypedPropertyDescriptor, 12 | ) => { 13 | if (propertyKey && descriptor) { 14 | MetadataHelper.setMetadata( 15 | rendererViewFile, 16 | fileName, 17 | descriptor.value, 18 | ); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/kv-queue/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../metadata/mod.ts'; 2 | import { queueListenerMetadataKey } from './constants.ts'; 3 | 4 | /** 5 | * Method decorator that registers a method as a listener for a specific queue channel. 6 | * 7 | * @param channel - The name of the queue channel to listen to. 8 | * @returns A method decorator function. 9 | */ 10 | export const OnQueueMessage = (channel: string): MethodDecorator => { 11 | return (_target, _propertyKey, descriptor) => { 12 | MetadataHelper.setMetadata( 13 | queueListenerMetadataKey, 14 | { channel }, 15 | descriptor.value, 16 | ); 17 | return descriptor; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/events/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../metadata/mod.ts'; 2 | import { eventListenerMetadataKey } from './constants.ts'; 3 | 4 | /** 5 | * A decorator that registers a method as an event listener for a specified channel. 6 | * 7 | * @param channel - The name of the event channel to listen to. 8 | * @returns A method decorator that registers the method as an event listener. 9 | */ 10 | export const OnEvent = (channel: string): MethodDecorator => { 11 | return (_target, _propertyKey, descriptor) => { 12 | MetadataHelper.setMetadata( 13 | eventListenerMetadataKey, 14 | { channel }, 15 | descriptor.value, 16 | ); 17 | return descriptor; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** A clear and 10 | concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** A clear and concise description of what you 13 | want to happen. 14 | 15 | **Describe alternatives you've considered** A clear and concise description of 16 | any alternative solutions or features you've considered. 17 | 18 | **Additional context** Add any other context or screenshots about the feature 19 | request here. 20 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | export { green, red, white, yellow } from '@std/fmt/colors'; 2 | export { Reflect } from 'deno_reflect'; 3 | export { validateObject } from '../validation.ts'; 4 | export { 5 | type Context, 6 | Hono as Application, 7 | type MiddlewareHandler, 8 | type Next, 9 | } from '@hono'; 10 | export { type HandlerInterface } from '@hono/types'; 11 | export { HonoRequest } from '@hono/request'; 12 | export { getPath } from '@hono/utils/url'; 13 | export { RegExpRouter } from '@hono/router/reg-exp-router'; 14 | export { SmartRouter } from '@hono/router/smart-router'; 15 | export { TrieRouter } from '@hono/router/trie-router'; 16 | export { SSEStreamingApi, streamSSE } from '@hono/streaming'; 17 | export { cors } from '@hono/cors'; 18 | -------------------------------------------------------------------------------- /src/injector/injectable/helper.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../../metadata/helper.ts'; 2 | import { Constructor } from '../../utils/constructor.ts'; 3 | import { InjectableOption, injectionData, SCOPE } from './decorator.ts'; 4 | 5 | /** 6 | * A helper class for injectable services. 7 | */ 8 | export class InjectableHelper { 9 | /** 10 | * Determines if the given constructor is global. 11 | * 12 | * @param constructor - The constructor to check. 13 | * @returns `true` if the constructor is global, otherwise `false`. 14 | */ 15 | static isGlobal(constructor: Constructor): boolean { 16 | const data = MetadataHelper.getMetadata( 17 | injectionData, 18 | constructor, 19 | ); 20 | return !data || !data.scope || data?.scope === SCOPE.GLOBAL; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/handlebar.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from './interface.ts'; 2 | import { Handlebars } from '@danet/handlebars'; 3 | 4 | const defaultOption = { 5 | baseDir: 'views', 6 | extname: '.hbs', 7 | layoutsDir: 'layouts/', 8 | partialsDir: 'partials/', 9 | cachePartials: true, 10 | defaultLayout: 'main', 11 | helpers: undefined, 12 | compilerOptions: undefined, 13 | }; 14 | 15 | export class HandlebarRenderer implements Renderer { 16 | private hbs: Handlebars; 17 | 18 | constructor() { 19 | this.hbs = new Handlebars(defaultOption); 20 | } 21 | 22 | setRootDir(rootDirectory: string) { 23 | this.hbs = new Handlebars({ 24 | ...defaultOption, 25 | baseDir: rootDirectory, 26 | }); 27 | } 28 | 29 | render(filename: string, data: Record): Promise { 30 | return this.hbs.renderView(filename, data); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/base-path.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, path } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Module } from '../src/module/decorator.ts'; 4 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 5 | 6 | @Controller('todo') 7 | class MyController { 8 | @Get('') 9 | simpleGet() { 10 | return 'hello'; 11 | } 12 | } 13 | 14 | @Module({ 15 | controllers: [MyController], 16 | }) 17 | class MyModule {} 18 | 19 | Deno.test('base path is registered', async () => { 20 | const app = new DanetApplication(); 21 | app.registerBasePath('/api/'); 22 | await app.init(MyModule); 23 | const listenEvent = await app.listen(0); 24 | 25 | const res = await fetch(`http://localhost:${listenEvent.port}/api/todo`, { 26 | method: 'GET', 27 | }); 28 | const text = await res.text(); 29 | assertEquals(text, 'hello'); 30 | await app.close(); 31 | }); 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Danet 2 | 3 | 🤟 Thanks for coming here and contributing to Danet 🤟 4 | 5 | ## How do I contribute : 6 | 7 | - Create an issue for bug or feature request 8 | - Comment an existing issue to say that you are tackling it 9 | - Join [our discord](https://discord.gg/Q7ZHuDPgjA) 10 | 11 | ## I don't know where to start 12 | 13 | Look for issues with 'good first issue'. If there is none but you are willing to 14 | contribute, hop on discord to talk about what can be done on the project. There 15 | is always things to do ! 16 | 17 | ### I am new to programming/typescript, I'm afraid to make mistake 18 | 19 | Do not worry. We all had to learn at some point. The best thing you can do it 20 | pick an issue and give it a try. 21 | 22 | If you feel lost or don't know what to do or how to do something, come on our 23 | discord and we will guide you. We might even do pair programming together ! 24 | -------------------------------------------------------------------------------- /spec/cors.test.ts: -------------------------------------------------------------------------------- 1 | import { DanetApplication } from '../src/app.ts'; 2 | import { Module } from '../src/module/decorator.ts'; 3 | import { assertEquals } from '../src/deps_test.ts'; 4 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 5 | 6 | const app = new DanetApplication(); 7 | app.enableCors(); 8 | 9 | @Controller('todo') 10 | class MyController { 11 | @Get('') 12 | simpleGet() { 13 | return 'hello'; 14 | } 15 | } 16 | 17 | @Module({ 18 | controllers: [MyController], 19 | }) 20 | class MyModule {} 21 | Deno.test('Options should answer 204', async () => { 22 | let res; 23 | try { 24 | await app.init(MyModule); 25 | const listenEvent = await app.listen(0); 26 | 27 | res = await fetch( 28 | `http://localhost:${listenEvent.port}/`, 29 | { 30 | method: 'OPTIONS', 31 | }, 32 | ); 33 | assertEquals(res.status, 204); 34 | } catch (e) { 35 | console.log(e); 36 | } 37 | await res?.body?.cancel(); 38 | await app.close(); 39 | }); 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** Steps to reproduce the behavior: 12 | 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior** A clear and concise description of what you expected to 19 | happen. 20 | 21 | **Screenshots** If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | 25 | - OS: [e.g. iOS] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | 29 | **Smartphone (please complete the following information):** 30 | 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /src/injector/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../metadata/helper.ts'; 2 | import { DecoratorFunction } from '../mod.ts'; 3 | 4 | export const injectionTokenMetadataKey = 'injection-token'; 5 | 6 | export function getInjectionTokenMetadataKey(parameterIndex: number): string { 7 | return `${injectionTokenMetadataKey}:${parameterIndex}`; 8 | } 9 | 10 | /** 11 | * Decorator to inject using token. 12 | * 13 | * Get example here https://danet.land/fundamentals/dynamic-modules.html#module-configuration 14 | * 15 | * @param token - Optional token to identify the dependency. 16 | * @returns A decorator function that sets the metadata for the injection token. 17 | */ 18 | export function Inject(token?: string): DecoratorFunction { 19 | return ( 20 | // deno-lint-ignore no-explicit-any 21 | target: Record | any, 22 | propertyKey: string | symbol | undefined, 23 | parameterIndex: number, 24 | ) => { 25 | MetadataHelper.setMetadata( 26 | getInjectionTokenMetadataKey(parameterIndex), 27 | token, 28 | target, 29 | ); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /spec/response-decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { All, Controller, Delete, Get, HttpCode, Patch, Post, Put } from '../src/router/controller/decorator.ts'; 2 | import { Module } from '../src/module/decorator.ts'; 3 | import { DanetApplication } from '../src/app.ts'; 4 | import { assertEquals } from '../src/deps_test.ts'; 5 | 6 | @Controller('nice-controller') 7 | class SimpleController { 8 | @Get('/') 9 | simpleGet() { 10 | return new Response('OK GET', { status: 201 }); 11 | } 12 | } 13 | 14 | @Module({ 15 | controllers: [SimpleController], 16 | }) 17 | class MyModule {} 18 | 19 | Deno.test('HttpCode', async () => { 20 | const app = new DanetApplication(); 21 | await app.init(MyModule); 22 | const listenEvent = await app.listen(0); 23 | 24 | const res = await fetch( 25 | `http://localhost:${listenEvent.port}/nice-controller`, 26 | { 27 | method: 'GET', 28 | }, 29 | ); 30 | const text = await res.text(); 31 | assertEquals(text, `OK GET`); 32 | assertEquals(res.status, 201); 33 | await app.close(); 34 | }); -------------------------------------------------------------------------------- /example/events.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | DanetApplication, 4 | EventEmitter, 5 | EventEmitterModule, 6 | Module, 7 | OnEvent, 8 | Post, 9 | } from '../mod.ts'; 10 | 11 | type User = {}; 12 | 13 | class UserListeners { 14 | @OnEvent('new-user') 15 | notifyUser(user: User) { 16 | console.log('new user created', user); 17 | } 18 | 19 | @OnEvent('new-user') 20 | async sendWelcomeEmail(user: User) { 21 | console.log('send email', user); 22 | } 23 | } 24 | 25 | @Controller('user') 26 | class UserController { 27 | constructor( 28 | private eventEmitter: EventEmitter, 29 | ) {} 30 | 31 | @Post() 32 | create() { 33 | const user: User = {}; 34 | this.eventEmitter.emit('new-user', user); 35 | return JSON.stringify(user); 36 | } 37 | } 38 | 39 | @Module({ 40 | imports: [EventEmitterModule], 41 | controllers: [UserController], 42 | injectables: [UserListeners], 43 | }) 44 | class AppModule {} 45 | 46 | const app = new DanetApplication(); 47 | await app.init(AppModule); 48 | 49 | let port = Number(Deno.env.get('PORT')); 50 | if (isNaN(port)) { 51 | port = 3000; 52 | } 53 | app.listen(port); 54 | -------------------------------------------------------------------------------- /src/utils/filepath.ts: -------------------------------------------------------------------------------- 1 | type FilePathOptions = { 2 | filename: string; 3 | root?: string; 4 | defaultDocument?: string; 5 | }; 6 | 7 | export const getFilePath = (options: FilePathOptions): string | undefined => { 8 | let filename = options.filename; 9 | if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) return; 10 | 11 | let root = options.root || ''; 12 | const defaultDocument = options.defaultDocument || 'index.html'; 13 | 14 | if (filename.endsWith('/')) { 15 | // /top/ => /top/index.html 16 | filename = filename.concat(defaultDocument); 17 | } else if (!filename.match(/\.[a-zA-Z0-9]+$/)) { 18 | // /top => /top/index.html 19 | filename = filename.concat('/' + defaultDocument); 20 | } 21 | 22 | // /foo.html => foo.html 23 | filename = filename.replace(/^\.?[\/\\]/, ''); 24 | 25 | // foo\bar.txt => foo/bar.txt 26 | filename = filename.replace(/\\/, '/'); 27 | 28 | // assets/ => assets 29 | root = root.replace(/\/$/, ''); 30 | 31 | // ./assets/foo.html => assets/foo.html 32 | let path = root ? root + '/' + filename : filename; 33 | path = path.replace(/^\.?\//, ''); 34 | 35 | return path; 36 | }; 37 | -------------------------------------------------------------------------------- /example/schedule.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cron, 3 | DanetApplication, 4 | Interval, 5 | IntervalExpression, 6 | Module, 7 | ScheduleModule, 8 | Timeout, 9 | } from '../mod.ts'; 10 | 11 | class TaskScheduler { 12 | getNow() { 13 | return { 14 | now: new Date(), 15 | }; 16 | } 17 | 18 | @Timeout(IntervalExpression.SECOND) 19 | runOnce() { 20 | console.log('run once after 1s', this.getNow()); 21 | } 22 | 23 | @Interval(IntervalExpression.SECOND) 24 | runEachSecond() { 25 | console.log('1 sec', this.getNow()); 26 | } 27 | 28 | @Cron('*/1 * * * *') 29 | runEachMinute() { 30 | console.log('1 minute', this.getNow()); 31 | } 32 | 33 | @Cron('*/2 * * * *') 34 | runEach2Min() { 35 | console.log('2 minutes', this.getNow()); 36 | } 37 | 38 | @Cron('*/3 * * * *') 39 | runEach3Min() { 40 | console.log('3 minutes', this.getNow()); 41 | } 42 | } 43 | 44 | @Module({ 45 | imports: [ScheduleModule], 46 | injectables: [TaskScheduler], 47 | }) 48 | class AppModule {} 49 | 50 | const app = new DanetApplication(); 51 | await app.init(AppModule); 52 | 53 | let port = Number(Deno.env.get('PORT')); 54 | if (isNaN(port)) { 55 | port = 3000; 56 | } 57 | app.listen(port); 58 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | --- 6 | 7 | ## Issue Ticket Number 8 | 9 | Fixes #(issue_number) 10 | 11 | --- 12 | 13 | ## Type of change 14 | 15 | 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] New feature (non-breaking change which adds functionality) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to 20 | not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | --- 24 | 25 | # Checklist: 26 | 27 | - [ ] I have run `deno lint` AND `deno fmt` AND `deno task test` and got no 28 | errors. 29 | - [ ] I have followed the contributing guidelines of this project as mentioned 30 | in [CONTRIBUTING.md](/CONTRIBUTING.md) 31 | - [ ] I have checked to ensure there aren't other open 32 | [Pull Requests](https://github.com/Savory/Danet/pulls) for the same 33 | update/change? 34 | - [ ] I have performed a self-review of my own code 35 | - [ ] I have made corresponding changes needed to the documentation 36 | -------------------------------------------------------------------------------- /spec/view-renderer.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, path } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Module } from '../src/module/decorator.ts'; 4 | import { Render } from '../src/renderer/decorator.ts'; 5 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 6 | import { HandlebarRenderer } from '../src/renderer/handlebar.ts'; 7 | 8 | @Controller('nice-controller') 9 | class SimpleController { 10 | @Render('index') 11 | @Get('/') 12 | simpleGet() { 13 | return { title: 'my title' }; 14 | } 15 | } 16 | 17 | @Module({ 18 | controllers: [SimpleController], 19 | }) 20 | class MyModule {} 21 | 22 | Deno.test('Hbs renderer', async () => { 23 | const app = new DanetApplication(); 24 | await app.init(MyModule); 25 | const viewPath = path.dirname(path.fromFileUrl(import.meta.url)) + '/views'; 26 | app.setRenderer(new HandlebarRenderer()) 27 | app.setViewEngineDir(viewPath); 28 | const listenEvent = await app.listen(0); 29 | 30 | const res = await fetch( 31 | `http://localhost:${listenEvent.port}/nice-controller`, 32 | { 33 | method: 'GET', 34 | }, 35 | ); 36 | const text = await res.text(); 37 | assertEquals(text.includes('my title'), true); 38 | await app.close(); 39 | }); 40 | -------------------------------------------------------------------------------- /src/injector/injectable/constructor.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../../utils/constructor.ts'; 2 | 3 | /** 4 | * Type Alias for Constructor 5 | */ 6 | export type InjectableConstructor = Constructor; 7 | 8 | /** @deprecated Prefer plain object of Type UseClassInjector */ 9 | export class TokenInjector { 10 | constructor(public useClass: InjectableConstructor, public token: string) { 11 | } 12 | } 13 | /** 14 | * Represents an injector configuration that uses a class constructor for dependency injection. 15 | * 16 | * @typedef {Object} UseClassInjector 17 | * @property {InjectableConstructor} useClass - The class constructor to be used for injection. 18 | * @property {string} token - The token that identifies the dependency. 19 | */ 20 | export type UseClassInjector = { 21 | useClass: InjectableConstructor; 22 | token: string; 23 | }; 24 | /** 25 | * Represents an injector that uses a specific value for dependency injection. 26 | * 27 | * @typedef {Object} UseValueInjector 28 | * @property {any} useValue - The value to be used for injection. 29 | * @property {string} token - The token that identifies the value. 30 | */ 31 | export type UseValueInjector = { 32 | // deno-lint-ignore no-explicit-any 33 | useValue: any; 34 | token: string; 35 | }; 36 | -------------------------------------------------------------------------------- /src/schedule/decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '../metadata/decorator.ts'; 2 | import { 3 | intervalMetadataKey, 4 | scheduleMetadataKey, 5 | timeoutMetadataKey, 6 | } from './constants.ts'; 7 | import { CronString } from './types.ts'; 8 | 9 | /** 10 | * Assigns a cron schedule to a method. The method will be executed according to the provided cron schedule. 11 | * 12 | * @param cron - A string representing the cron schedule. 13 | * @returns A method decorator that sets the metadata key with the provided cron schedule. 14 | */ 15 | export const Cron = (cron: CronString): MethodDecorator => 16 | SetMetadata(scheduleMetadataKey, { cron }); 17 | 18 | /** 19 | * Method will be executed at a specified interval. 20 | * 21 | * @param interval - The interval in milliseconds at which the method should be executed. 22 | * @returns A method decorator that sets the interval metadata. 23 | */ 24 | export const Interval = (interval: number): MethodDecorator => 25 | SetMetadata(intervalMetadataKey, { interval }); 26 | 27 | /** 28 | * Execute Method after X milliseconds. 29 | * 30 | * @param timeout - The timeout duration in milliseconds. 31 | * @returns A method decorator that sets the timeout metadata. 32 | */ 33 | export const Timeout = (timeout: number): MethodDecorator => 34 | SetMetadata(timeoutMetadataKey, { timeout }); 35 | -------------------------------------------------------------------------------- /src/kv-queue/kv.ts: -------------------------------------------------------------------------------- 1 | import { OnAppBootstrap, OnAppClose } from '../hook/mod.ts'; 2 | import { Inject } from '../mod.ts'; 3 | import { Injectable } from '../mod.ts'; 4 | import { KV_QUEUE_NAME, QueueEvent } from './constants.ts'; 5 | 6 | // deno-lint-ignore no-explicit-any 7 | type Listener

= (payload: P) => void; 8 | 9 | @Injectable() 10 | export class KvQueue implements OnAppClose, OnAppBootstrap { 11 | private kv!: Deno.Kv; 12 | private listenersMap: Map = new Map(); 13 | 14 | constructor(@Inject(KV_QUEUE_NAME) private name: string) { 15 | } 16 | 17 | public async onAppClose(): Promise { 18 | await this.kv.close(); 19 | } 20 | 21 | public async onAppBootstrap(): Promise { 22 | this.kv = await Deno.openKv(this.name); 23 | } 24 | 25 | public sendMessage(type: string, data: unknown): Promise { 26 | return this.kv.enqueue({ type, data }); 27 | } 28 | 29 | public addListener(type: string, callback: Listener): void { 30 | this.listenersMap.set(type, callback); 31 | } 32 | 33 | public attachListeners(): void { 34 | this.kv.listenQueue((msg: QueueEvent) => { 35 | const type = msg.type; 36 | const callback = this.listenersMap.get(type); 37 | if (callback) { 38 | return callback(msg.data); 39 | } 40 | throw Error('Unhandled message type'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/exception/filter/decorator.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../../utils/constructor.ts'; 2 | import { MetadataFunction, SetMetadata } from '../../metadata/decorator.ts'; 3 | 4 | /** 5 | * Metadata key used to mark and identify exception filters. 6 | * 7 | * @constant {string} filterExceptionMetadataKey 8 | */ 9 | export const filterExceptionMetadataKey = 'filterException'; 10 | /** 11 | * A decorator function that applies a specified filter to the metadata of a class or method. 12 | * 13 | * @param filter - The constructor of the filter to be applied. 14 | * @returns A function that sets the metadata for the filter. 15 | */ 16 | export function UseFilter(filter: Constructor): MetadataFunction { 17 | return SetMetadata(filterExceptionMetadataKey, filter); 18 | } 19 | /** 20 | * Used to store metadata for the type of errors caught by a filter. 21 | * 22 | * @constant 23 | * @type {string} 24 | */ 25 | export const filterCatchTypeMetadataKey = 'errorCaught'; 26 | /** 27 | * A decorator function to specify the error type to catch for an exception filter. 28 | * 29 | * @param ErrorType - The constructor of the error type to catch. 30 | * @returns A metadata function that sets the metadata for the error type to catch. 31 | */ 32 | export function Catch(ErrorType: Constructor): MetadataFunction { 33 | return SetMetadata(filterCatchTypeMetadataKey, ErrorType); 34 | } 35 | -------------------------------------------------------------------------------- /src/hook/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Hook interfaces. 4 | * Provide lifecycle hooks interface for the application. 5 | * https://danet.land/fundamentals/lifecycle.html 6 | */ 7 | 8 | import { HttpContext } from '../router/router.ts'; 9 | 10 | /** 11 | * Interface representing a hook that is called when the application is bootstrapped. 12 | * https://danet.land/fundamentals/lifecycle.html 13 | */ 14 | export interface OnAppBootstrap { 15 | onAppBootstrap(): void | Promise; 16 | } 17 | 18 | /** 19 | * Interface representing a handler for application shutdown events. 20 | */ 21 | export interface OnAppClose { 22 | onAppClose(): void | Promise; 23 | } 24 | 25 | /** 26 | * Interface representing a hook that is called before a controller method is invoked. 27 | * Useful for Request Scoped Services. 28 | */ 29 | export interface BeforeControllerMethodIsCalled { 30 | beforeControllerMethodIsCalled(ctx?: HttpContext): void | Promise; 31 | } 32 | 33 | /** 34 | * Enum representing the names of various hooks in the application. 35 | * 36 | * @enum {string} 37 | * @property {string} APP_CLOSE - Hook triggered when the application is closing. 38 | * @property {string} APP_BOOTSTRAP - Hook triggered when the application is bootstrapping. 39 | */ 40 | export enum hookName { 41 | APP_CLOSE = 'onAppClose', 42 | APP_BOOTSTRAP = 'onAppBootstrap', 43 | } 44 | -------------------------------------------------------------------------------- /src/exception/http/enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for HTTP status codes 3 | */ 4 | export enum HTTP_STATUS { 5 | CONTINUE = 100, 6 | SWITCHING_PROTOCOLS = 101, 7 | PROCESSING = 102, 8 | EARLYHINTS = 103, 9 | OK = 200, 10 | CREATED = 201, 11 | ACCEPTED = 202, 12 | NON_AUTHORITATIVE_INFORMATION = 203, 13 | NO_CONTENT = 204, 14 | RESET_CONTENT = 205, 15 | PARTIAL_CONTENT = 206, 16 | AMBIGUOUS = 300, 17 | MOVED_PERMANENTLY = 301, 18 | FOUND = 302, 19 | SEE_OTHER = 303, 20 | NOT_MODIFIED = 304, 21 | TEMPORARY_REDIRECT = 307, 22 | PERMANENT_REDIRECT = 308, 23 | BAD_REQUEST = 400, 24 | UNAUTHORIZED = 401, 25 | PAYMENT_REQUIRED = 402, 26 | FORBIDDEN = 403, 27 | NOT_FOUND = 404, 28 | METHOD_NOT_ALLOWED = 405, 29 | NOT_ACCEPTABLE = 406, 30 | PROXY_AUTHENTICATION_REQUIRED = 407, 31 | REQUEST_TIMEOUT = 408, 32 | CONFLICT = 409, 33 | GONE = 410, 34 | LENGTH_REQUIRED = 411, 35 | PRECONDITION_FAILED = 412, 36 | PAYLOAD_TOO_LARGE = 413, 37 | URI_TOO_LONG = 414, 38 | UNSUPPORTED_MEDIA_TYPE = 415, 39 | REQUESTED_RANGE_NOT_SATISFIABLE = 416, 40 | EXPECTATION_FAILED = 417, 41 | I_AM_A_TEAPOT = 418, 42 | MISDIRECTED = 421, 43 | UNPROCESSABLE_ENTITY = 422, 44 | FAILED_DEPENDENCY = 424, 45 | PRECONDITION_REQUIRED = 428, 46 | TOO_MANY_REQUESTS = 429, 47 | INTERNAL_SERVER_ERROR = 500, 48 | NOT_IMPLEMENTED = 501, 49 | BAD_GATEWAY = 502, 50 | SERVICE_UNAVAILABLE = 503, 51 | GATEWAY_TIMEOUT = 504, 52 | HTTP_VERSION_NOT_SUPPORTED = 505, 53 | } 54 | -------------------------------------------------------------------------------- /src/router/controller/params/resolver.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../../../metadata/helper.ts'; 2 | import { ControllerConstructor } from '../constructor.ts'; 3 | import { 4 | Resolver, 5 | } from './decorators.ts'; 6 | import { ExecutionContext } from '../../mod.ts'; 7 | import { argumentResolverFunctionsMetadataKey } from './constants.ts'; 8 | 9 | /** 10 | * Resolves the parameters for a given controller method by using metadata to map 11 | * parameter indices to resolver functions. 12 | * 13 | * @param Controller - The constructor of the controller class. 14 | * @param ControllerMethod - The method of the controller for which parameters need to be resolved. 15 | * @param context - The execution context which may be used by resolver functions to resolve parameters. 16 | * @returns A promise that resolves to an array of parameters for the controller method. 17 | */ 18 | export async function resolveMethodParam( 19 | Controller: ControllerConstructor, 20 | // deno-lint-ignore no-explicit-any 21 | ControllerMethod: (...args: any[]) => unknown, 22 | context: ExecutionContext, 23 | ) { 24 | const paramResolverMap: Map = MetadataHelper.getMetadata( 25 | argumentResolverFunctionsMetadataKey, 26 | Controller, 27 | ControllerMethod.name, 28 | ); 29 | const params: unknown[] = []; 30 | if (paramResolverMap) { 31 | for (const [key, value] of paramResolverMap) { 32 | params[key] = await value(context); 33 | } 34 | } 35 | return params; 36 | } 37 | -------------------------------------------------------------------------------- /src/injector/injectable/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataFunction, SetMetadata } from '../../metadata/decorator.ts'; 2 | 3 | /** 4 | * The different scopes for dependency injection. 5 | * 6 | * @enum {string} 7 | * @property {string} GLOBAL - Represents a global scope where the instance is shared across the entire application. 8 | * @property {string} REQUEST - Represents a request scope where a new instance is created for each request. 9 | * @property {string} TRANSIENT - Represents a transient scope where a new instance is created every time it is requested. 10 | */ 11 | export enum SCOPE { 12 | GLOBAL = 'GLOBAL', 13 | REQUEST = 'REQUEST', 14 | TRANSIENT = 'TRANSIENT', 15 | } 16 | 17 | /** 18 | * Options for the Injectable decorator. 19 | * 20 | * @interface InjectableOption 21 | * 22 | * @property {SCOPE} scope - The scope in which the injectable should be instantiated. 23 | */ 24 | export interface InjectableOption { 25 | scope: SCOPE; 26 | } 27 | 28 | export const injectionData = 'dependency-injection'; 29 | 30 | /** 31 | * Mark class as an injectable. 32 | * 33 | * @template T - The type of the class being decorated. 34 | * @param {InjectableOption} [options={ scope: SCOPE.GLOBAL }] - The options for the injectable, including the scope. 35 | * @returns {MetadataFunction} - A function that sets the metadata for the injectable. 36 | */ 37 | export function Injectable( 38 | options: InjectableOption = { scope: SCOPE.GLOBAL }, 39 | ): MetadataFunction { 40 | return SetMetadata(injectionData, options); 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/serve-static.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Next } from '../deps.ts'; 2 | import { getFilePath } from './filepath.ts'; 3 | import { getMimeType } from './get-mime.ts'; 4 | const { open } = Deno; 5 | 6 | export type ServeStaticOptions = { 7 | root?: string; 8 | path?: string; 9 | rewriteRequestPath?: (path: string) => string; 10 | }; 11 | 12 | export const serveStatic = (options: ServeStaticOptions = { root: '' }) => { 13 | return async (c: Context, next: Next) => { 14 | // Do nothing if Response is already set 15 | if (c.finalized) { 16 | await next(); 17 | return; 18 | } 19 | // hey tomato love u 20 | const url = new URL(c.req.url); 21 | const filename = options.path ?? decodeURI(url.pathname); 22 | let path = getFilePath({ 23 | filename: options.rewriteRequestPath 24 | ? options.rewriteRequestPath(filename) 25 | : filename, 26 | root: options.root, 27 | defaultDocument: 'index.html', 28 | }); 29 | 30 | if (!path) return await next(); 31 | 32 | if (Deno.build.os !== 'windows') { 33 | path = `/${path}`; 34 | } 35 | 36 | let file; 37 | 38 | try { 39 | file = await open(path); 40 | } catch (e) { 41 | console.warn(`${e}`); 42 | } 43 | 44 | if (file) { 45 | const mimeType = getMimeType(path); 46 | if (mimeType) { 47 | c.header('Content-Type', mimeType); 48 | } 49 | // Return Response object with stream 50 | return c.body(file.readable); 51 | } else { 52 | console.warn(`Static file: ${path} is not found`); 53 | await next(); 54 | } 55 | return; 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /src/metadata/decorator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | ** metadataDecorator 3 | ** Provides decorator to set Metadata 4 | ** @module 5 | */ 6 | 7 | import { ControllerConstructor } from '../router/controller/constructor.ts'; 8 | import { MetadataHelper } from './helper.ts'; 9 | 10 | /** 11 | * A function type that represents a metadata decorator. 12 | * 13 | * @param target - The target object or constructor to which the metadata is applied. 14 | * @param propertyKey - An optional property key for the target. 15 | * @param descriptor - An optional property descriptor for the target. 16 | */ 17 | export type MetadataFunction = ( 18 | // deno-lint-ignore ban-types 19 | target: ControllerConstructor | Object, 20 | propertyKey?: string | symbol, 21 | // deno-lint-ignore no-explicit-any 22 | descriptor?: TypedPropertyDescriptor, 23 | ) => void; 24 | 25 | /** 26 | * Sets metadata on the target object or method. 27 | * 28 | * @param key - The key for the metadata. 29 | * @param value - The value for the metadata. 30 | * @returns a MetadaFunction. 31 | */ 32 | export const SetMetadata = (key: string, value: unknown): MetadataFunction => 33 | ( 34 | // deno-lint-ignore ban-types 35 | target: ControllerConstructor | Object, 36 | propertyKey?: string | symbol, 37 | // deno-lint-ignore no-explicit-any 38 | descriptor?: TypedPropertyDescriptor, 39 | ): void => { 40 | if (propertyKey && descriptor) { 41 | MetadataHelper.setMetadata( 42 | key, 43 | value, 44 | descriptor.value, 45 | ); 46 | } else { 47 | MetadataHelper.setMetadata(key, value, target); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno then run Deno lint and test. 7 | # For more information see: https://github.com/denoland/setup-deno 8 | 9 | name: Run tests 10 | 11 | on: 12 | push: 13 | branches: [main] 14 | pull_request: 15 | branches: [main] 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | version: ['v1.x', canary] 28 | steps: 29 | - name: Setup repo 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup Deno 33 | # uses: denoland/setup-deno@v1 34 | uses: denoland/setup-deno@v1 35 | with: 36 | deno-version: ${{ matrix.version }} 37 | 38 | # Uncomment this step to verify the use of 'deno fmt' on each commit. 39 | # - name: Verify formatting 40 | # run: deno fmt --check 41 | 42 | - name: Run linter 43 | run: deno lint 44 | 45 | - name: Run tests 46 | run: deno task test 47 | - name: Create coverage report 48 | run: deno coverage --unstable ./coverage --lcov --exclude="test\.(ts|tsx|mts|js|mjs|jsx|cjs|cts)|exceptions\.ts|logger\.ts" > coverage.lcov 49 | - name: Collect coverage 50 | uses: codecov/codecov-action@v1.0.10 # upload the report on Codecov 51 | with: 52 | file: ./coverage.lcov 53 | -------------------------------------------------------------------------------- /spec/render-static-files.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, path } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Module } from '../src/module/decorator.ts'; 4 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 5 | 6 | @Controller('todo') 7 | class MyController { 8 | @Get('') 9 | simpleGet() { 10 | return 'hello'; 11 | } 12 | } 13 | 14 | @Module({ 15 | controllers: [MyController], 16 | }) 17 | class MyModule {} 18 | 19 | Deno.test('it serve static files', async () => { 20 | const app = new DanetApplication(); 21 | await app.init(MyModule); 22 | const staticAssetsPath = path.dirname(path.fromFileUrl(import.meta.url)) + 23 | '/static'; 24 | app.useStaticAssets(staticAssetsPath); 25 | const listenEvent = await app.listen(0); 26 | 27 | const res = await fetch(`http://localhost:${listenEvent.port}/test.txt`, { 28 | method: 'GET', 29 | }); 30 | const blob = await res.blob(); 31 | const text = await blob.text(); 32 | console.log(text); 33 | assertEquals(text.indexOf('I love pikachu'), 0); 34 | await app.close(); 35 | }); 36 | 37 | Deno.test('serving static file does not break routes', async () => { 38 | const app = new DanetApplication(); 39 | await app.init(MyModule); 40 | const staticAssetsPath = path.dirname(path.fromFileUrl(import.meta.url)) + 41 | '/static'; 42 | app.useStaticAssets(staticAssetsPath); 43 | const listenEvent = await app.listen(0); 44 | 45 | const res = await fetch(`http://localhost:${listenEvent.port}/todo`, { 46 | method: 'GET', 47 | }); 48 | const text = await res.text(); 49 | assertEquals(text, 'hello'); 50 | await app.close(); 51 | }); 52 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@danet/core", 3 | "version": "2.9.6", 4 | "exports": { 5 | ".": "./mod.ts", 6 | "./metadata": "./src/metadata/mod.ts", 7 | "./validation": "./validation.ts", 8 | "./hook": "./src/hook/mod.ts", 9 | "./logger": "./src/logger.ts" 10 | }, 11 | "lint": { 12 | "include": [ 13 | "src/" 14 | ], 15 | "exclude": [ 16 | "node_modules/", 17 | "./**/*.test.ts" 18 | ], 19 | "rules": { 20 | "tags": [ 21 | "recommended" 22 | ], 23 | "include": [ 24 | "ban-untagged-todo" 25 | ], 26 | "exclude": [ 27 | "no-unused-vars" 28 | ] 29 | } 30 | }, 31 | "fmt": { 32 | "options": { 33 | "singleQuote": true, 34 | "useTabs": true 35 | }, 36 | "exclude": [ 37 | "./node_modules/", 38 | "./coverage/", 39 | "./doc/" 40 | ] 41 | }, 42 | "compilerOptions": { 43 | "emitDecoratorMetadata": true, 44 | "experimentalDecorators": true, 45 | "jsx": "react-jsx", 46 | "jsxImportSource": "preact" 47 | }, 48 | "tasks": { 49 | "test": "NO_LOG=true deno test -A --unstable-kv --unstable-cron --coverage=coverage spec/**/*.test.ts", 50 | "start:example": "deno run --allow-net --allow-env --watch example/run.ts" 51 | }, 52 | "imports": { 53 | "@std/testing": "jsr:@std/testing@0.223.0", 54 | "validatte": "jsr:@danet/validatte@0.7.4", 55 | "@std/path": "jsr:@std/path@0.223.0", 56 | "@std/fmt": "jsr:@std/fmt@0.223.0", 57 | "deno_reflect": "jsr:@dx/reflect@0.2.14", 58 | "@hono": "jsr:@hono/hono@4.6.3", 59 | "@danet/handlebars": "jsr:@danet/handlebars@0.0.1" 60 | }, 61 | "publish": { 62 | "exclude": ["./coverage/", "./spec", ".github", ".vscode", "./example"] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/events/module.ts: -------------------------------------------------------------------------------- 1 | import { OnAppBootstrap, OnAppClose } from '../hook/interfaces.ts'; 2 | import { MetadataHelper } from '../metadata/helper.ts'; 3 | import { injector, Logger, Module } from '../mod.ts'; 4 | import { eventListenerMetadataKey } from './constants.ts'; 5 | import { EventEmitter } from './events.ts'; 6 | 7 | /* Use this module if you want to use EventEmitter https://danet.land/techniques/events.html */ 8 | @Module({ 9 | injectables: [EventEmitter], 10 | }) 11 | export class EventEmitterModule implements OnAppBootstrap, OnAppClose { 12 | private logger: Logger = new Logger('EventEmitterModule'); 13 | 14 | constructor() {} 15 | 16 | onAppBootstrap(): void | Promise { 17 | for (const instance of injector.injectables) { 18 | this.registerAvailableEventListeners(instance); 19 | } 20 | } 21 | 22 | onAppClose() { 23 | const emitter = injector.get(EventEmitter); 24 | emitter.unsubscribe(); 25 | } 26 | 27 | // deno-lint-ignore no-explicit-any 28 | private registerAvailableEventListeners(injectableInstance: any) { 29 | const methods = Object.getOwnPropertyNames( 30 | injectableInstance.constructor.prototype, 31 | ); 32 | const emitter = injector.get(EventEmitter); 33 | 34 | for (const method of methods) { 35 | const target = injectableInstance[method]; 36 | const eventListenerMedatada = MetadataHelper.getMetadata< 37 | { channel: string } 38 | >( 39 | eventListenerMetadataKey, 40 | target, 41 | ); 42 | if (!eventListenerMedatada) continue; 43 | const { channel } = eventListenerMedatada; 44 | 45 | emitter.subscribe(channel, target.bind(injectableInstance)); 46 | this.logger.log(`registering method '${method}' to event '${channel}'`); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spec/sse.test.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Module, SSE } from '../mod.ts'; 2 | import { assertEquals } from '../src/deps_test.ts'; 3 | import { DanetApplication } from '../src/app.ts'; 4 | import { SSEEvent } from '../src/sse/event.ts'; 5 | 6 | @Controller() 7 | class SSEExampleController { 8 | @SSE('sse') 9 | sendUpdate(): EventTarget { 10 | const eventTarget = new EventTarget(); 11 | let id = 0; 12 | const interval = setInterval(() => { 13 | if (id >= 4) { 14 | clearInterval(interval); 15 | const event = new SSEEvent({ 16 | retry: 1000, 17 | id: `${id}`, 18 | data: 'close', 19 | event: 'close', 20 | }); 21 | eventTarget.dispatchEvent(event); 22 | return; 23 | } 24 | const event = new SSEEvent({ 25 | retry: 1000, 26 | id: `${id}`, 27 | data: 'world', 28 | event: 'hello', 29 | }); 30 | eventTarget.dispatchEvent(event); 31 | id++; 32 | }, 100); 33 | return eventTarget; 34 | } 35 | } 36 | 37 | @Module({ 38 | controllers: [SSEExampleController], 39 | }) 40 | class ExampleModule {} 41 | 42 | Deno.test('Body', async () => { 43 | return new Promise(async (resolve, reject) => { 44 | const app = new DanetApplication(); 45 | await app.init(ExampleModule); 46 | const listenEvent = await app.listen(0); 47 | let eventReceived = 0; 48 | const eventSource = new EventSource( 49 | `http://localhost:${listenEvent.port}/sse`, 50 | ); 51 | 52 | eventSource.addEventListener('hello', async (event) => { 53 | if (event.data === 'world') { 54 | eventReceived++; 55 | } 56 | }); 57 | 58 | eventSource.addEventListener('close', async (event) => { 59 | assertEquals(eventReceived, 4); 60 | await eventSource.close(); 61 | await app.close(); 62 | resolve(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/router/middleware/decorator.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareHandler as HonoMiddleware } from '../../deps.ts'; 2 | import { ExecutionContext } from '../router.ts'; 3 | import { InjectableConstructor } from '../../injector/injectable/constructor.ts'; 4 | import { MetadataFunction, SetMetadata } from '../../metadata/decorator.ts'; 5 | 6 | /** 7 | * Interface representing a middleware. 8 | * 9 | * @interface DanetMiddleware 10 | * 11 | * @method action 12 | * @param {ExecutionContext} ctx - The execution context for the middleware. 13 | * @param {NextFunction} next - The next function to call in the middleware chain. 14 | * @returns {Promise | unknown} - A promise or a value indicating the result of the middleware action. 15 | */ 16 | export interface DanetMiddleware { 17 | action(ctx: ExecutionContext, next: NextFunction): Promise | unknown; 18 | } 19 | 20 | /** 21 | * Represents a function that, when called, proceeds to the next middleware in the chain. 22 | * 23 | * @returns A promise that resolves to either void or a Response object. 24 | */ 25 | export type NextFunction = () => Promise; 26 | 27 | export type MiddlewareFunction = ( 28 | ctx: ExecutionContext, 29 | next: NextFunction, 30 | ) => unknown; 31 | 32 | export type PossibleMiddlewareType = 33 | | InjectableConstructor 34 | | HonoMiddleware 35 | | MiddlewareFunction; 36 | export const isMiddlewareClass = (s: PossibleMiddlewareType) => !!s.prototype; 37 | export const middlewareMetadataKey = 'middlewares'; 38 | /** 39 | * A decorator function that attaches middleware to a route handler or controller. 40 | * 41 | * @param {...PossibleMiddlewareType[]} middlewares - A list of middleware functions to be applied. 42 | * @returns {MetadataFunction} - A function that sets the metadata for the middleware. 43 | */ 44 | export function Middleware( 45 | ...middlewares: PossibleMiddlewareType[] 46 | ): MetadataFunction { 47 | return SetMetadata(middlewareMetadataKey, middlewares); 48 | } 49 | -------------------------------------------------------------------------------- /src/hook/executor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Hook executor. 4 | * Provides a class to execute hooks for every injectable. 5 | */ 6 | 7 | import { InjectableHelper } from '../injector/injectable/helper.ts'; 8 | import { Injector } from '../injector/injector.ts'; 9 | import { MetadataHelper } from '../metadata/helper.ts'; 10 | import { hookName } from './interfaces.ts'; 11 | 12 | /** 13 | * The `HookExecutor` class is responsible for executing hooks on all injectables 14 | * retrieved from the provided `Injector`. It ensures that hooks are executed 15 | * only on instances that are objects and are marked as global. 16 | */ 17 | export class HookExecutor { 18 | /** 19 | * Creates an instance of `HookExecutor`. 20 | * @param injector - The injector instance used to retrieve all injectables. 21 | */ 22 | constructor(private injector: Injector) {} 23 | 24 | /** 25 | * Executes a specified hook on every injectable retrieved from the injector. 26 | * It iterates through all injectables, checks if they are objects, and then 27 | * executes the hook on each instance. 28 | * 29 | * @param hookName - The name of the hook to be executed. 30 | */ 31 | public async executeHookForEveryInjectable(hookName: hookName) { 32 | const injectables = this.injector.getAll(); 33 | for (const [_, value] of injectables) { 34 | const instanceOrValue: unknown = value(); 35 | if (!MetadataHelper.IsObject(instanceOrValue)) { 36 | continue; 37 | } 38 | await this.executeInstanceHook(instanceOrValue, hookName); 39 | } 40 | } 41 | 42 | /** 43 | * Executes a specified hook on a given instance if the instance is marked as global. 44 | * 45 | * @param instance - The instance on which the hook is to be executed. 46 | * @param hookName - The name of the hook to be executed. 47 | */ 48 | 49 | // deno-lint-ignore no-explicit-any 50 | private async executeInstanceHook(instance: any, hookName: hookName) { 51 | if (InjectableHelper.isGlobal(instance?.constructor)) { 52 | await instance?.[hookName]?.(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/kv-queue/module.ts: -------------------------------------------------------------------------------- 1 | import { OnAppBootstrap, OnAppClose } from '../hook/interfaces.ts'; 2 | import { MetadataHelper } from '../metadata/helper.ts'; 3 | import { 4 | InjectableConstructor, 5 | injector, 6 | Logger, 7 | Module, 8 | ModuleConstructor, 9 | TokenInjector, 10 | UseValueInjector, 11 | } from '../mod.ts'; 12 | import { KV_QUEUE_NAME, queueListenerMetadataKey } from './constants.ts'; 13 | import { KvQueue } from './kv.ts'; 14 | 15 | /* Use this module if you want to use KV Queue https://danet.land/techniques/kvQueue.html */ 16 | @Module({}) 17 | export class KvQueueModule implements OnAppBootstrap { 18 | private logger: Logger = new Logger('QueueModule'); 19 | 20 | constructor() {} 21 | 22 | public static forRoot(kvName?: string): { 23 | injectables: Array< 24 | InjectableConstructor | TokenInjector | UseValueInjector 25 | >; 26 | module: typeof KvQueueModule; 27 | } { 28 | return { 29 | injectables: [{ token: KV_QUEUE_NAME, useValue: kvName }, KvQueue], 30 | module: KvQueueModule, 31 | }; 32 | } 33 | 34 | onAppBootstrap(): void { 35 | for (const instanceOrPlainValue of injector.injectables) { 36 | if (!MetadataHelper.IsObject(instanceOrPlainValue)) { 37 | continue; 38 | } 39 | this.registerAvailableEventListeners(instanceOrPlainValue); 40 | } 41 | } 42 | 43 | // deno-lint-ignore no-explicit-any 44 | private registerAvailableEventListeners(injectableInstance: any) { 45 | const methods = Object.getOwnPropertyNames( 46 | injectableInstance.constructor.prototype, 47 | ); 48 | const queue = injector.get(KvQueue); 49 | 50 | for (const method of methods) { 51 | const target = injectableInstance[method]; 52 | const queueListenerMetadata = MetadataHelper.getMetadata< 53 | { channel: string } 54 | >( 55 | queueListenerMetadataKey, 56 | target, 57 | ); 58 | if (!queueListenerMetadata) continue; 59 | const { channel } = queueListenerMetadata; 60 | 61 | queue.addListener(channel, target); 62 | this.logger.log( 63 | `registering method '${method}' to queue channel '${channel}'`, 64 | ); 65 | } 66 | queue.attachListeners(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/run.ts: -------------------------------------------------------------------------------- 1 | import { 2 | All, 3 | Body, 4 | Controller, 5 | DanetApplication, 6 | Get, 7 | Injectable, 8 | Module, 9 | Param, 10 | Post, 11 | Query, 12 | Req, 13 | SCOPE, 14 | } from '../src/mod.ts'; 15 | 16 | @Injectable() 17 | class SharedService { 18 | sum(nums: number[]): number { 19 | return nums.reduce((sum, n) => sum + n, 0); 20 | } 21 | } 22 | 23 | @Injectable({ scope: SCOPE.REQUEST }) 24 | class ScopedService2 { 25 | getWorldHello() { 26 | return 'World Hello'; 27 | } 28 | } 29 | 30 | @Injectable({ scope: SCOPE.REQUEST }) 31 | class ScopedService1 { 32 | constructor(public child: ScopedService2) { 33 | } 34 | getHelloWorld(name: string) { 35 | return `Hello World ${name}`; 36 | } 37 | } 38 | 39 | @Controller('') 40 | class FirstController { 41 | constructor( 42 | private sharedService: SharedService, 43 | private scopedService1: ScopedService1, 44 | private scopedService2: ScopedService2, 45 | ) { 46 | } 47 | 48 | @Get('') 49 | getMethod() { 50 | return 'OK'; 51 | } 52 | 53 | @Get('hello-world/:name') 54 | getHelloWorld( 55 | @Param('name') name: string, 56 | ) { 57 | return this.scopedService1.getHelloWorld(name); 58 | } 59 | 60 | @Get('world-hello') 61 | getWorldHello() { 62 | return this.scopedService2.getWorldHello(); 63 | } 64 | 65 | @Get('sum') 66 | getSum( 67 | @Query('num', { value: 'array' }) numParams: string | string[], 68 | ) { 69 | const numString = Array.isArray(numParams) ? numParams : [numParams]; 70 | return this.sharedService.sum(numString.map((n) => Number(n))); 71 | } 72 | 73 | @Post('post') 74 | // deno-lint-ignore no-explicit-any 75 | postMethod(@Body() body: any) { 76 | return body; 77 | } 78 | 79 | @All('/all') 80 | allHandler(@Req() req: Request) { 81 | return req.method; 82 | } 83 | } 84 | 85 | @Module({ 86 | controllers: [FirstController], 87 | injectables: [SharedService, ScopedService1, ScopedService2], 88 | }) 89 | class FirstModule {} 90 | 91 | const app = new DanetApplication(); 92 | await app.init(FirstModule); 93 | 94 | let port = Number(Deno.env.get('PORT')); 95 | if (isNaN(port)) { 96 | port = 3000; 97 | } 98 | app.listen(port); 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Danet Copyright (c) 2022 Thomas CRUVEILHER 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 | 23 | 24 | NestJS Copyright (c) 2017-2022 Kamil Myśliwiec http://kamilmysliwiec.com 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | -------------------------------------------------------------------------------- /src/module/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../metadata/helper.ts'; 2 | import { ControllerConstructor } from '../router/controller/constructor.ts'; 3 | import { InjectableConstructor } from '../injector/injectable/constructor.ts'; 4 | import { Constructor } from '../utils/constructor.ts'; 5 | import { ModuleConstructor } from './constructor.ts'; 6 | import { UseClassInjector, UseValueInjector } from '../mod.ts'; 7 | 8 | /** 9 | * Metadata for a module. 10 | * 11 | * https://danet.land/overview/modules.html 12 | * 13 | * @property {Array} [imports] - Optional array of modules or dynamic modules to be imported. 14 | * @property {ControllerConstructor[]} [controllers] - Optional array of controller constructors. 15 | * @property {Array} [injectables] - Optional array of injectables, which can be constructors, value injectors, or class injectors. 16 | */ 17 | export interface ModuleMetadata { 18 | imports?: Array; 19 | controllers?: ControllerConstructor[]; 20 | injectables?: Array< 21 | InjectableConstructor | UseValueInjector | UseClassInjector 22 | >; 23 | } 24 | 25 | /** 26 | * Represents a dynamic module in the application. 27 | * 28 | * https://danet.land/fundamentals/dynamic-modules.html 29 | * 30 | * @interface DynamicModule 31 | * @extends {ModuleMetadata} 32 | * 33 | * @property {ModuleConstructor} module - The constructor of the module. 34 | */ 35 | export interface DynamicModule extends ModuleMetadata { 36 | module: ModuleConstructor; 37 | } 38 | 39 | export const moduleMetadataKey = 'module'; 40 | 41 | /** 42 | * Module Decorator. 43 | * 44 | * https://danet.land/overview/modules.html 45 | * 46 | * @template T - The type of the class to which the metadata will be attached. 47 | * @param {ModuleMetadata} options - The metadata options to be attached to the class. 48 | * @returns {(Type: Constructor) => void} - A function that takes a class constructor and attaches the metadata to it. 49 | */ 50 | export function Module( 51 | options: ModuleMetadata, 52 | ): (Type: Constructor) => void { 53 | return (Type: Constructor): void => { 54 | MetadataHelper.setMetadata(moduleMetadataKey, options, Type); 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /spec/queue.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | DanetApplication, 4 | Get, 5 | KvQueue, 6 | KvQueueModule, 7 | Module, 8 | OnQueueMessage, 9 | } from '../mod.ts'; 10 | import { 11 | assertEquals, 12 | assertSpyCall, 13 | assertSpyCalls, 14 | spy, 15 | } from '../src/deps_test.ts'; 16 | 17 | const sleep = (msec: number) => 18 | new Promise((resolve) => setTimeout(resolve, msec)); 19 | 20 | Deno.test('Queue Module', async (t) => { 21 | const callback = spy((_payload: any) => {}); 22 | const secondCallback = spy((_payload: any) => {}); 23 | const payload = { name: 'test' }; 24 | 25 | class TestListener { 26 | @OnQueueMessage('trigger') 27 | getSomething(payload: any) { 28 | callback(payload); 29 | } 30 | 31 | @OnQueueMessage('another-trigger') 32 | getSomethingAgain(payload: any) { 33 | secondCallback(payload); 34 | } 35 | } 36 | 37 | @Controller('trigger') 38 | class TestController { 39 | constructor(private queue: KvQueue) {} 40 | 41 | @Get() 42 | getSomething() { 43 | this.queue.sendMessage('trigger', payload); 44 | return 'OK'; 45 | } 46 | 47 | @Get('again') 48 | getSomethingAgain() { 49 | this.queue.sendMessage('another-trigger', 'toto'); 50 | return 'OK'; 51 | } 52 | } 53 | 54 | @Module({ 55 | imports: [KvQueueModule.forRoot()], 56 | controllers: [TestController], 57 | injectables: [TestListener], 58 | }) 59 | class TestModule {} 60 | const application = new DanetApplication(); 61 | try { 62 | await application.init(TestModule); 63 | const listenerInfo = await application.listen(0); 64 | assertEquals(callback.calls.length, 0); 65 | 66 | let res = await fetch(`http://localhost:${listenerInfo.port}/trigger`); 67 | 68 | assertEquals(res.status, 200); 69 | assertEquals(await res.text(), 'OK'); 70 | 71 | await sleep(500); 72 | assertSpyCalls(callback, 1); 73 | assertSpyCall(callback, 0, { 74 | args: [payload], 75 | }); 76 | 77 | res = await fetch(`http://localhost:${listenerInfo.port}/trigger/again`); 78 | 79 | assertEquals(res.status, 200); 80 | assertEquals(await res.text(), 'OK'); 81 | await sleep(500); 82 | assertEquals(secondCallback.calls.length, 1); 83 | assertSpyCall(secondCallback, 0, { 84 | args: ['toto'], 85 | }); 86 | await application.close(); 87 | } catch (e) { 88 | await application.close(); 89 | throw e; 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * Logger module. 4 | * Provides a class to log messages to the console. 5 | */ 6 | 7 | import { green, red, white, yellow } from './deps.ts'; 8 | import { Injectable } from './injector/injectable/decorator.ts'; 9 | 10 | /** 11 | * A Logger class to handle logging with optional namespace and color-coded output. 12 | * 13 | * @remarks 14 | * This class provides methods to log messages with different severity levels (log, error, warn). 15 | * It supports optional namespaces for better context in logs and uses color functions to 16 | * differentiate log types. 17 | * 18 | * @example 19 | * ```typescript 20 | * const logger = new Logger('MyNamespace'); 21 | * logger.log('This is an info message'); 22 | * logger.error('This is an error message'); 23 | * logger.warn('This is a warning message'); 24 | * ``` 25 | */ 26 | @Injectable() 27 | export class Logger { 28 | constructor(private namespace?: string) { 29 | } 30 | 31 | private setNamespace(namespace: string) { 32 | this.namespace = namespace; 33 | } 34 | 35 | private getNamespaceForDisplay() { 36 | if (this.namespace) { 37 | return `[${this.namespace}] `; 38 | } 39 | return ''; 40 | } 41 | 42 | private concatNamespaceAndText( 43 | text: string, 44 | colorFunc: (text: string) => string, 45 | ) { 46 | const date = new Date().toUTCString(); 47 | const context = this.getNamespaceForDisplay(); 48 | 49 | return `${white(date)} ${yellow(context)} ${colorFunc(text)}`; 50 | } 51 | 52 | private printTo( 53 | text: string, 54 | colorFunc: (text: string) => string, 55 | loggingFunc: (text: string) => void, 56 | ) { 57 | if (Deno.env.get('NO_LOG')) { 58 | return; 59 | } 60 | loggingFunc(this.concatNamespaceAndText(text, colorFunc)); 61 | } 62 | 63 | /** 64 | * Logs a message to the console with green color. 65 | * 66 | * @param text - The message to be logged. 67 | */ 68 | log(text: string) { 69 | this.printTo(text, green, console.log); 70 | } 71 | 72 | /** 73 | * Logs an error message to the console with a red color. 74 | * 75 | * @param text - The error message to be logged. 76 | */ 77 | error(text: string) { 78 | this.printTo(text, red, console.error); 79 | } 80 | 81 | /** 82 | * Logs a warning message to the console with a yellow color. 83 | * 84 | * @param text - The warning message to be logged. 85 | */ 86 | warn(text: string) { 87 | this.printTo(text, yellow, console.warn); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /spec/websocket/method-param-decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthGuard, 3 | Body, 4 | DanetApplication, 5 | ExecutionContext, 6 | ExpectationFailedException, 7 | Injectable, 8 | Module, 9 | Param, 10 | UseGuard, 11 | } from '../../mod.ts'; 12 | import { assertEquals } from '../../src/deps_test.ts'; 13 | import { 14 | OnWebSocketMessage, 15 | WebSocketController, 16 | } from '../../src/router/websocket/decorator.ts'; 17 | 18 | @WebSocketController('ws') 19 | class ExampleController { 20 | constructor() { 21 | } 22 | 23 | @OnWebSocketMessage('hello') 24 | sayHello(@Body() whateverIsSent: any) { 25 | return { topic: 'hello', data: whateverIsSent }; 26 | } 27 | 28 | @OnWebSocketMessage('hello/:name') 29 | sayHelloWithName(@Param('name') name: string) { 30 | return { topic: 'hello', data: name }; 31 | } 32 | } 33 | 34 | @Module({ 35 | controllers: [ExampleController], 36 | injectables: [], 37 | }) 38 | class ExampleModule {} 39 | 40 | Deno.test('Body', async () => { 41 | return new Promise(async (resolve) => { 42 | const app = new DanetApplication(); 43 | await app.init(ExampleModule); 44 | const listenEvent = await app.listen(0); 45 | 46 | const websocket = new WebSocket( 47 | `ws://localhost:${listenEvent.port}/ws?token=goodToken`, 48 | ); 49 | websocket.onmessage = async (e) => { 50 | const parsedResponse = JSON.parse(e.data); 51 | assertEquals(parsedResponse.topic, 'hello'); 52 | assertEquals(parsedResponse.data.didIGetThisBack, true); 53 | await app.close(); 54 | websocket.close(); 55 | resolve(); 56 | }; 57 | websocket.onopen = (e) => { 58 | websocket.send( 59 | JSON.stringify({ topic: 'hello', data: { didIGetThisBack: true } }), 60 | ); 61 | }; 62 | }); 63 | }); 64 | 65 | Deno.test('Param', async () => { 66 | return new Promise(async (resolve) => { 67 | const app = new DanetApplication(); 68 | await app.init(ExampleModule); 69 | const listenEvent = await app.listen(0); 70 | 71 | const websocket = new WebSocket( 72 | `ws://localhost:${listenEvent.port}/ws?token=goodToken`, 73 | ); 74 | websocket.onmessage = async (e) => { 75 | const parsedResponse = JSON.parse(e.data); 76 | assertEquals(parsedResponse.topic, 'hello'); 77 | assertEquals(parsedResponse.data, 'thomas'); 78 | await app.close(); 79 | websocket.close(); 80 | resolve(); 81 | }; 82 | websocket.onopen = (e) => { 83 | websocket.send( 84 | JSON.stringify({ topic: 'hello/thomas', data: 'unrelevant' }), 85 | ); 86 | }; 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /spec/lifecycle-hook.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { OnAppBootstrap, OnAppClose } from '../src/hook/interfaces.ts'; 4 | import { Injectable, SCOPE } from '../src/injector/injectable/decorator.ts'; 5 | import { Module } from '../src/module/decorator.ts'; 6 | import { Controller } from '../src/router/controller/decorator.ts'; 7 | 8 | Deno.test('Lifecycle hooks', async (testContext) => { 9 | let moduleOnAppBootstrapCalled = false; 10 | 11 | @Injectable({ scope: SCOPE.GLOBAL }) 12 | class InjectableWithHook implements OnAppBootstrap, OnAppClose { 13 | public appBoostrapCalled = 0; 14 | public appCloseCalled = 0; 15 | onAppBootstrap() { 16 | this.appBoostrapCalled += 1; 17 | } 18 | onAppClose() { 19 | this.appCloseCalled += 1; 20 | } 21 | } 22 | 23 | @Controller('second-controller/') 24 | class ControllerWithHook implements OnAppBootstrap, OnAppClose { 25 | public appBoostrapCalled = 0; 26 | public appCloseCalled = 0; 27 | onAppBootstrap(): void | Promise { 28 | this.appBoostrapCalled += 1; 29 | } 30 | onAppClose() { 31 | this.appCloseCalled += 1; 32 | } 33 | 34 | constructor( 35 | public child2: InjectableWithHook, 36 | ) { 37 | } 38 | } 39 | 40 | @Module({ 41 | controllers: [ControllerWithHook], 42 | injectables: [ 43 | InjectableWithHook, 44 | ], 45 | }) 46 | class MyModule implements OnAppBootstrap { 47 | onAppBootstrap(): void | Promise { 48 | moduleOnAppBootstrapCalled = true; 49 | } 50 | } 51 | 52 | const app = new DanetApplication(); 53 | await app.init(MyModule); 54 | 55 | await testContext.step( 56 | 'call global injectables onAppBootstrap hook', 57 | async () => { 58 | const injectableWithHook = await app.get(InjectableWithHook); 59 | assertEquals(injectableWithHook.appBoostrapCalled, 1); 60 | }, 61 | ); 62 | 63 | await testContext.step('call module onAppBoostrap hook', () => { 64 | assertEquals(moduleOnAppBootstrapCalled, true); 65 | }); 66 | 67 | await testContext.step( 68 | 'call global controller onAppBootstrap hook', 69 | async () => { 70 | const controllerWithHook = await app.get(ControllerWithHook); 71 | assertEquals(controllerWithHook.appBoostrapCalled, 1); 72 | }, 73 | ); 74 | 75 | await testContext.step( 76 | 'call injectables and controllers onAppClosehook when app is closed', 77 | async () => { 78 | await app.close(); 79 | const injectableWithHook = await app.get(ControllerWithHook); 80 | const controllerWithHook = await app.get(InjectableWithHook); 81 | assertEquals(controllerWithHook.appCloseCalled, 1); 82 | assertEquals(injectableWithHook.appCloseCalled, 1); 83 | }, 84 | ); 85 | }); 86 | -------------------------------------------------------------------------------- /spec/websocket/auth-guard.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthGuard, 3 | DanetApplication, 4 | ExecutionContext, 5 | ExpectationFailedException, 6 | Injectable, 7 | Module, 8 | UseGuard, 9 | } from '../../mod.ts'; 10 | import { assertEquals } from '../../src/deps_test.ts'; 11 | import { 12 | OnWebSocketMessage, 13 | WebSocketController, 14 | } from '../../src/router/websocket/decorator.ts'; 15 | 16 | @Injectable() 17 | class ExampleAuthGuard implements AuthGuard { 18 | async canActivate(context: ExecutionContext) { 19 | if (context.req.query('token') !== 'goodToken') { 20 | return false; 21 | } 22 | return true; 23 | } 24 | } 25 | 26 | @Injectable() 27 | class ControllerGuard implements AuthGuard { 28 | async canActivate(context: ExecutionContext) { 29 | if (context.websocketMessage.secret === 'whateversecret') { 30 | return true; 31 | } 32 | return false; 33 | } 34 | } 35 | 36 | @Injectable() 37 | class ExampleService { 38 | sayHello() { 39 | return 'coucou'; 40 | } 41 | } 42 | 43 | @UseGuard(ExampleAuthGuard) 44 | @WebSocketController('ws') 45 | class ExampleController { 46 | constructor(private service: ExampleService) { 47 | } 48 | 49 | @UseGuard(ControllerGuard) 50 | @OnWebSocketMessage('hello') 51 | sayHello() { 52 | return { topic: 'hello', data: this.service.sayHello() }; 53 | } 54 | } 55 | 56 | @Module({ 57 | controllers: [ExampleController], 58 | injectables: [ExampleService], 59 | }) 60 | class ExampleModule {} 61 | 62 | Deno.test('Websocket', async () => { 63 | return new Promise(async (resolve) => { 64 | const app = new DanetApplication(); 65 | await app.init(ExampleModule); 66 | const listenEvent = await app.listen(0); 67 | 68 | const unauthorizedWebsocket = new WebSocket( 69 | `ws://localhost:${listenEvent.port}/ws?token=notagoodtoken`, 70 | ); 71 | unauthorizedWebsocket.onclose = (e) => { 72 | assertEquals(e.reason, 'Unauthorized'); 73 | }; 74 | 75 | const missingSecretwebsocket = new WebSocket( 76 | `ws://localhost:${listenEvent.port}/ws?token=goodToken`, 77 | ); 78 | missingSecretwebsocket.onopen = (e) => { 79 | missingSecretwebsocket.send( 80 | JSON.stringify({ topic: 'hello', data: { secret: 'notagoodsecret' } }), 81 | ); 82 | }; 83 | missingSecretwebsocket.onclose = (e) => { 84 | assertEquals(e.reason, 'Unauthorized'); 85 | }; 86 | const websocket = new WebSocket( 87 | `ws://localhost:${listenEvent.port}/ws?token=goodToken`, 88 | ); 89 | websocket.onmessage = async (e) => { 90 | const parsedResponse = JSON.parse(e.data); 91 | assertEquals(parsedResponse.topic, 'hello'); 92 | assertEquals(parsedResponse.data, 'coucou'); 93 | await app.close(); 94 | websocket.close(); 95 | resolve(); 96 | }; 97 | websocket.onopen = (e) => { 98 | websocket.send( 99 | JSON.stringify({ topic: 'hello', data: { secret: 'whateversecret' } }), 100 | ); 101 | }; 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /spec/schedule.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Cron, 3 | CronExpression, 4 | DanetApplication, 5 | Interval, 6 | IntervalExpression, 7 | Module, 8 | ScheduleModule, 9 | Timeout, 10 | } from '../mod.ts'; 11 | import { assertEquals } from '../src/deps_test.ts'; 12 | import { assertSpyCallArg, FakeTime, spy } from '../src/deps_test.ts'; 13 | 14 | Deno.test('Schedule Module', async (t) => { 15 | const cron = Deno.cron; 16 | // @ts-ignore:next-line 17 | Deno.cron = spy(); 18 | 19 | class TestListener { 20 | @Cron(CronExpression.EVERY_MINUTE) 21 | runEachMinute() {} 22 | } 23 | 24 | @Module({ 25 | imports: [ScheduleModule], 26 | injectables: [TestListener], 27 | }) 28 | class TestModule {} 29 | 30 | const application = new DanetApplication(); 31 | await application.init(TestModule); 32 | await application.listen(0); 33 | 34 | await t.step('cronjob was called', () => { 35 | // @ts-ignore:next-line 36 | assertSpyCallArg(Deno.cron, 0, 0, 'runEachMinute'); 37 | // @ts-ignore:next-line 38 | assertSpyCallArg(Deno.cron, 0, 1, CronExpression.EVERY_MINUTE); 39 | }); 40 | 41 | await application.close(); 42 | Deno.cron = cron; 43 | }); 44 | 45 | Deno.test('Timeout Module', async (t) => { 46 | const time = new FakeTime(); 47 | const cb = spy(); 48 | 49 | class TestListener { 50 | @Timeout(IntervalExpression.MILISECOND) 51 | runEachMinute() { 52 | cb(); 53 | } 54 | } 55 | 56 | @Module({ 57 | imports: [ScheduleModule], 58 | injectables: [TestListener], 59 | }) 60 | class TestModule {} 61 | 62 | const application = new DanetApplication(); 63 | await application.init(TestModule); 64 | await application.listen(0); 65 | 66 | await t.step('should be called once after tick', async () => { 67 | await time.tickAsync(IntervalExpression.MILISECOND); 68 | assertEquals(cb.calls.length, 1); 69 | 70 | await time.tickAsync(IntervalExpression.MILISECOND); 71 | assertEquals(cb.calls.length, 1); 72 | }); 73 | 74 | await application.close(); 75 | }); 76 | 77 | Deno.test('Interval Module', async (t) => { 78 | const time = new FakeTime(); 79 | const cb = spy(); 80 | 81 | class TestListener { 82 | @Interval(IntervalExpression.MILISECOND) 83 | runEachMinute() { 84 | cb(); 85 | } 86 | } 87 | 88 | @Module({ 89 | imports: [ScheduleModule], 90 | injectables: [TestListener], 91 | }) 92 | class TestModule {} 93 | 94 | const application = new DanetApplication(); 95 | await application.init(TestModule); 96 | await application.listen(0); 97 | 98 | await t.step('should be called once after tick', async () => { 99 | await time.tickAsync(IntervalExpression.MILISECOND); 100 | assertEquals(cb.calls.length, 1); 101 | 102 | await time.tickAsync(IntervalExpression.MILISECOND); 103 | assertEquals(cb.calls.length, 2); 104 | 105 | await time.tickAsync(IntervalExpression.MILISECOND); 106 | assertEquals(cb.calls.length, 3); 107 | }); 108 | 109 | await application.close(); 110 | }); 111 | -------------------------------------------------------------------------------- /src/utils/get-mime.ts: -------------------------------------------------------------------------------- 1 | export const getMimeType = (filename: string): string | undefined => { 2 | const regexp = /\.([a-zA-Z0-9]+?)$/; 3 | const match = filename.match(regexp); 4 | if (!match) return; 5 | let mimeType = mimes[match[1]]; 6 | if ( 7 | (mimeType && mimeType.startsWith('text')) || mimeType === 'application/json' 8 | ) { 9 | mimeType += '; charset=utf-8'; 10 | } 11 | return mimeType; 12 | }; 13 | 14 | const mimes: Record = { 15 | aac: 'audio/aac', 16 | abw: 'application/x-abiword', 17 | arc: 'application/x-freearc', 18 | avi: 'video/x-msvideo', 19 | avif: 'image/avif', 20 | av1: 'video/av1', 21 | azw: 'application/vnd.amazon.ebook', 22 | bin: 'application/octet-stream', 23 | bmp: 'image/bmp', 24 | bz: 'application/x-bzip', 25 | bz2: 'application/x-bzip2', 26 | csh: 'application/x-csh', 27 | css: 'text/css', 28 | csv: 'text/csv', 29 | doc: 'application/msword', 30 | docx: 31 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 32 | eot: 'application/vnd.ms-fontobject', 33 | epub: 'application/epub+zip', 34 | gif: 'image/gif', 35 | gz: 'application/gzip', 36 | htm: 'text/html', 37 | html: 'text/html', 38 | ico: 'image/x-icon', 39 | ics: 'text/calendar', 40 | jar: 'application/java-archive', 41 | jpeg: 'image/jpeg', 42 | jpg: 'image/jpeg', 43 | js: 'text/javascript', 44 | json: 'application/json', 45 | jsonld: 'application/ld+json', 46 | map: 'application/json', 47 | mid: 'audio/x-midi', 48 | midi: 'audio/x-midi', 49 | mjs: 'text/javascript', 50 | mp3: 'audio/mpeg', 51 | mp4: 'video/mp4', 52 | mpeg: 'video/mpeg', 53 | mpkg: 'application/vnd.apple.installer+xml', 54 | odp: 'application/vnd.oasis.opendocument.presentation', 55 | ods: 'application/vnd.oasis.opendocument.spreadsheet', 56 | odt: 'application/vnd.oasis.opendocument.text', 57 | oga: 'audio/ogg', 58 | ogv: 'video/ogg', 59 | ogx: 'application/ogg', 60 | opus: 'audio/opus', 61 | otf: 'font/otf', 62 | pdf: 'application/pdf', 63 | php: 'application/php', 64 | png: 'image/png', 65 | ppt: 'application/vnd.ms-powerpoint', 66 | pptx: 67 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 68 | rtf: 'application/rtf', 69 | sh: 'application/x-sh', 70 | svg: 'image/svg+xml', 71 | swf: 'application/x-shockwave-flash', 72 | tar: 'application/x-tar', 73 | tif: 'image/tiff', 74 | tiff: 'image/tiff', 75 | ts: 'video/mp2t', 76 | ttf: 'font/ttf', 77 | txt: 'text/plain', 78 | vsd: 'application/vnd.visio', 79 | wasm: 'application/wasm', 80 | webm: 'video/webm', 81 | weba: 'audio/webm', 82 | webp: 'image/webp', 83 | woff: 'font/woff', 84 | woff2: 'font/woff2', 85 | xhtml: 'application/xhtml+xml', 86 | xls: 'application/vnd.ms-excel', 87 | xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 88 | xml: 'application/xml', 89 | xul: 'application/vnd.mozilla.xul+xml', 90 | zip: 'application/zip', 91 | '3gp': 'video/3gpp', 92 | '3g2': 'video/3gpp2', 93 | '7z': 'application/x-7z-compressed', 94 | gltf: 'model/gltf+json', 95 | glb: 'model/gltf-binary', 96 | }; 97 | -------------------------------------------------------------------------------- /src/metadata/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * metadata 3 | * Provides metadata helper functions 4 | * Dependencies: Reflect 5 | * @module 6 | */ 7 | 8 | import { Reflect } from '../deps.ts'; 9 | 10 | /** 11 | * A helper class for managing metadata on objects and their properties. 12 | * 13 | * This class provides static methods to determine if a value is an object, 14 | * retrieve metadata from an object or its property, and set metadata on an 15 | * object or its property. 16 | * 17 | * @example 18 | * ```ts 19 | * // Check if a value is an object 20 | * const isObject = MetadataHelper.IsObject(someValue); 21 | * 22 | * // Retrieve metadata from an object 23 | * const metadataValue = MetadataHelper.getMetadata('metadataKey', someObject); 24 | * 25 | * // Set metadata on an object 26 | * MetadataHelper.setMetadata('metadataKey', 'metadataValue', someObject); 27 | * ``` 28 | */ 29 | export class MetadataHelper { 30 | /** 31 | * Determines if the provided value is an object. 32 | * 33 | * This function checks if the given value is of type 'object' and not null, 34 | * or if it is of type 'function'. 35 | * 36 | * @template T - The type of the value being checked. 37 | * @param x - The value to check. It can be of type T, undefined, null, boolean, string, symbol, or number. 38 | * @returns A boolean indicating whether the value is an object. 39 | */ 40 | static IsObject( 41 | x: T | undefined | null | boolean | string | symbol | number, 42 | ): x is T { 43 | return typeof x === 'object' ? x !== null : typeof x === 'function'; 44 | } 45 | 46 | /** 47 | * Retrieves metadata of a specified key from an object or its property. 48 | * 49 | * @template T - The expected type of the metadata value. 50 | * @param {string} key - The key for the metadata to retrieve. 51 | * @param {unknown} obj - The target object from which to retrieve the metadata. 52 | * @param {string | symbol} [property] - Optional. The property of the object from which to retrieve the metadata. 53 | * @returns {T} - The metadata value associated with the specified key. 54 | */ 55 | static getMetadata( 56 | key: string, 57 | obj: unknown, 58 | property?: string | symbol, 59 | ): T { 60 | if (property) { 61 | return Reflect.getOwnMetadata(key, obj, property); 62 | } 63 | return Reflect.getMetadata(key, obj) as T; 64 | } 65 | 66 | /** 67 | * Sets metadata for a target object or its property. 68 | * 69 | * @param key - The metadata key. 70 | * @param value - The metadata value. 71 | * @param target - The target object to set the metadata on. 72 | * @param property - Optional. The property of the target object to set the metadata on. 73 | * If not provided, the metadata is set on the target object itself. 74 | * 75 | * @returns void 76 | */ 77 | static setMetadata( 78 | key: string, 79 | value: unknown, 80 | target: unknown, 81 | property?: string | symbol, 82 | ): void { 83 | if (property) { 84 | return Reflect.defineMetadata(key, value, target, property); 85 | } 86 | return Reflect.defineMetadata(key, value, target); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spec/http-methods.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Module } from '../src/module/decorator.ts'; 4 | import { 5 | All, 6 | Controller, 7 | Delete, 8 | Get, 9 | Patch, 10 | Post, 11 | Put, HttpCode, 12 | } from '../src/router/controller/decorator.ts'; 13 | 14 | @Controller('nice-controller') 15 | class SimpleController { 16 | @Get('/') 17 | simpleGet() { 18 | return 'OK GET'; 19 | } 20 | 21 | @Post('/') 22 | simplePost() { 23 | return 'OK POST'; 24 | } 25 | 26 | @Put('/') 27 | simplePut() { 28 | return 'OK PUT'; 29 | } 30 | 31 | @Patch('/') 32 | simplePatch() { 33 | return 'OK PATCH'; 34 | } 35 | 36 | @Delete('/') 37 | simpleDelete() { 38 | return 'OK DELETE'; 39 | } 40 | 41 | @All('/all') 42 | all() { 43 | return 'OK ALL'; 44 | } 45 | 46 | @HttpCode(203) 47 | @Get('/custom-http-status') 48 | customHttpStatus() { 49 | return 'OK'; 50 | } 51 | 52 | @HttpCode(204) 53 | @Delete('/custom-http-status') 54 | async delete() { 55 | } 56 | } 57 | 58 | @Module({ 59 | controllers: [SimpleController], 60 | }) 61 | class MyModule {} 62 | 63 | for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) { 64 | Deno.test(method, async () => { 65 | const app = new DanetApplication(); 66 | await app.init(MyModule); 67 | const listenEvent = await app.listen(0); 68 | 69 | const res = await fetch( 70 | `http://localhost:${listenEvent.port}/nice-controller`, 71 | { 72 | method, 73 | }, 74 | ); 75 | const text = await res.text(); 76 | assertEquals(text, `OK ${method}`); 77 | await app.close(); 78 | }); 79 | } 80 | 81 | Deno.test('ALL', async () => { 82 | const app = new DanetApplication(); 83 | await app.init(MyModule); 84 | const listenEvent = await app.listen(0); 85 | 86 | for (const method of ['GET', 'POST', 'PUT', 'DELETE']) { 87 | const res = await fetch( 88 | `http://localhost:${listenEvent.port}/nice-controller/all`, 89 | { 90 | method: method, 91 | }, 92 | ); 93 | const text = await res.text(); 94 | assertEquals(text, `OK ALL`); 95 | } 96 | await app.close(); 97 | }); 98 | 99 | Deno.test('Custom HTTP status', async () => { 100 | const app = new DanetApplication(); 101 | await app.init(MyModule); 102 | const listenEvent = await app.listen(0); 103 | 104 | const res = await fetch( 105 | `http://localhost:${listenEvent.port}/nice-controller/custom-http-status`, 106 | { 107 | method: 'GET', 108 | }, 109 | ); 110 | const text = await res.text(); 111 | assertEquals(text, 'OK'); 112 | assertEquals(res.status, 203); 113 | await app.close(); 114 | }); 115 | 116 | Deno.test('Delete Custom HTTP status', async () => { 117 | const app = new DanetApplication(); 118 | await app.init(MyModule); 119 | const listenEvent = await app.listen(0); 120 | 121 | const res = await fetch( 122 | `http://localhost:${listenEvent.port}/nice-controller/custom-http-status`, 123 | { 124 | method: 'DELETE', 125 | }, 126 | ); 127 | const text = await res.text(); 128 | assertEquals(res.status, 204); 129 | await app.close(); 130 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Danet Logo 3 |

4 | 5 | [![CI](https://github.com/savory/Danet/actions/workflows/run-tests.yml/badge.svg)](https://github.com/savory/Danet/actions/workflows/run-tests.yml) 6 | [![codecov](https://codecov.io/gh/Savory/Danet/branch/main/graph/badge.svg?token=R6WXVC669Z)](https://codecov.io/gh/Savory/Danet) 7 | ![Made for Deno](https://img.shields.io/badge/made%20for-Deno-6B82F6?style=flat-square) 8 | [![JSR](https://jsr.io/badges/@danet/core)](https://jsr.io/@danet/core) 9 | 10 | ## Warning 11 | 12 | From version 2.4.0, Danet is only available via [JSR](https://jsr.io/) at 13 | [@danet/core](https://jsr.io/@danet/core). It's a step closer to runtime 14 | agnosticism. 15 | 16 | ## Description 17 | 18 | Danet is a framework heavily inspired by a NodeJS Framework called 19 | [Nest](https://docs.nestjs.com/). We aim to provide the same efficiency, but in 20 | [Deno](https://deno.land/). Of course, Nest is way more mature, think of it as a 21 | hero that we look up to. 22 | 23 | We borrow a lot from it, including documentation and sentences on this page, so 24 | please, definitely check it out because they deserve a lot of credit. Without 25 | Nest, we wouldn't be developing Danet. 26 | 27 | Danet is a framework for building efficient, scalable Deno server-side 28 | applications. It is entirely built with Typescript. 29 | 30 | Under the hood, Danet makes use of [Hono](https://hono.dev/). 31 | 32 | We abstract a lot of things so you can focus on your core business and 33 | architecture. 34 | 35 | ## Philosophy 36 | 37 | Deno is a relatively new engine. Nest was one of the greatest frameworks to 38 | improve the architecture of NodeJS project. We want to bring the same level of 39 | pro-efficiency and professionalism into Deno's world. 40 | 41 | Exactly like Nest, Danet provides an out-of-the-box application architecture 42 | which allows developers and teams to create highly testable, scalable, loosely 43 | coupled, and easily maintainable applications. 44 | 45 | The architecture is the same as our hero, and it was originally heavily inspired 46 | by Angular. 47 | 48 | ## Come with us on this awesome journey 49 | 50 | We always welcome contributors, feel free to submit a new feature or report a 51 | bug on our [Github Repository](https://github.com/Savory/Danet) and 52 | [join our discord](https://discord.gg/Q7ZHuDPgjA) 53 | 54 | ## Installation 55 | 56 | ```sh 57 | deno install --global -A -n danet jsr:@danet/cli 58 | danet new 59 | ``` 60 | 61 | ## Documentation 62 | 63 | [Use our CLI](https://github.com/Savory/Danet-CLI) 64 | 65 | [Read our documentation](https://danet.land) 66 | 67 | [Read our blog](https://savory.github.io/) 68 | 69 | ## Contributing 70 | 71 | - Contributions make the open-source community such an amazing place to learn, 72 | inspire, and create. 73 | - Any contributions you make are **truly appreciated**. 74 | - Check out our [contribution guidelines](/CONTRIBUTING.md) for more 75 | information. 76 | 77 |

78 | License 79 |

80 |
81 | 82 |

83 | This project is Licensed under the MIT License. Please go through the License at least once before making your contribution.

84 | -------------------------------------------------------------------------------- /spec/scoped-lifecycle-hook.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { BeforeControllerMethodIsCalled } from '../src/hook/interfaces.ts'; 4 | import { Injectable, SCOPE } from '../src/injector/injectable/decorator.ts'; 5 | import { Module } from '../src/module/decorator.ts'; 6 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 7 | import { HttpContext } from '../src/router/router.ts'; 8 | import { Inject } from '../src/injector/decorator.ts'; 9 | 10 | Deno.test('Scoped Lifecycle hooks', async (testContext) => { 11 | interface ScopedInjectableInterface { 12 | somethingThatMatters: string | null; 13 | } 14 | 15 | @Injectable({ scope: SCOPE.REQUEST }) 16 | class ScopedInjectable implements BeforeControllerMethodIsCalled { 17 | public somethingThatMatters: string | null = null; 18 | beforeControllerMethodIsCalled(ctx: HttpContext) { 19 | this.somethingThatMatters = `Received a ${ctx.req.method} request`; 20 | } 21 | } 22 | 23 | @Injectable() 24 | class InjectableUsingScoped { 25 | constructor( 26 | @Inject('SCOPED_TOKEN') public child1: ScopedInjectableInterface, 27 | ) { 28 | } 29 | getData() { 30 | return this.child1.somethingThatMatters; 31 | } 32 | } 33 | 34 | @Controller('side-effect/') 35 | class SideEffectController { 36 | @Get() 37 | public returnSomethingThatMatters(): string | null { 38 | return this.child1.getData(); 39 | } 40 | constructor( 41 | public child1: InjectableUsingScoped, 42 | ) { 43 | } 44 | } 45 | 46 | @Controller('scoped-controller/') 47 | class ScopedController { 48 | @Get() 49 | public returnSomethingThatMatters(): string | null { 50 | return this.child1.somethingThatMatters; 51 | } 52 | constructor( 53 | @Inject('SCOPED_TOKEN') public child1: ScopedInjectableInterface, 54 | ) { 55 | } 56 | } 57 | 58 | @Module({ 59 | controllers: [ScopedController, SideEffectController], 60 | injectables: [ 61 | InjectableUsingScoped, 62 | { 63 | useClass: ScopedInjectable, 64 | token: 'SCOPED_TOKEN', 65 | }, 66 | ], 67 | }) 68 | class ParentBeforeScopedModule {} 69 | 70 | await testContext.step( 71 | 'handleRequest is called before request when defined in a scoped service', 72 | async () => { 73 | const app = new DanetApplication(); 74 | await app.init(ParentBeforeScopedModule); 75 | const listenEvent = await app.listen(0); 76 | 77 | const res = await fetch( 78 | `http://localhost:${listenEvent.port}/scoped-controller/`, 79 | { 80 | method: 'GET', 81 | }, 82 | ); 83 | const text = await res.text(); 84 | assertEquals(text, `Received a GET request`); 85 | await app.close(); 86 | }, 87 | ); 88 | 89 | await testContext.step( 90 | 'handleRequest is called before request when defined in a scoped service but thats a side effect', 91 | async () => { 92 | const app = new DanetApplication(); 93 | await app.init(ParentBeforeScopedModule); 94 | const listenEvent = await app.listen(0); 95 | 96 | const res = await fetch( 97 | `http://localhost:${listenEvent.port}/side-effect/`, 98 | { 99 | method: 'GET', 100 | }, 101 | ); 102 | const text = await res.text(); 103 | assertEquals(text, `Received a GET request`); 104 | await app.close(); 105 | }, 106 | ); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/scoped-lifecycle-hook-another-order.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { BeforeControllerMethodIsCalled } from '../src/hook/interfaces.ts'; 4 | import { Injectable, SCOPE } from '../src/injector/injectable/decorator.ts'; 5 | import { Module } from '../src/module/decorator.ts'; 6 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 7 | import { HttpContext } from '../src/router/router.ts'; 8 | import { Inject } from '../src/injector/decorator.ts'; 9 | 10 | Deno.test('Scoped Lifecycle hooks other order', async (testContext) => { 11 | interface ScopedInjectableInterface { 12 | somethingThatMatters: string | null; 13 | } 14 | 15 | @Injectable({ scope: SCOPE.REQUEST }) 16 | class ScopedInjectable implements BeforeControllerMethodIsCalled { 17 | public somethingThatMatters: string | null = null; 18 | beforeControllerMethodIsCalled(ctx: HttpContext) { 19 | this.somethingThatMatters = `Received a ${ctx.req.method} request`; 20 | } 21 | } 22 | 23 | @Injectable() 24 | class InjectableUsingScoped { 25 | constructor( 26 | @Inject('SCOPED_TOKEN') public child1: ScopedInjectableInterface, 27 | ) { 28 | } 29 | getData() { 30 | return this.child1.somethingThatMatters; 31 | } 32 | } 33 | 34 | @Controller('side-effect/') 35 | class SideEffectController { 36 | @Get() 37 | public returnSomethingThatMatters(): string | null { 38 | return this.child1.getData(); 39 | } 40 | constructor( 41 | public child1: InjectableUsingScoped, 42 | ) { 43 | } 44 | } 45 | 46 | @Controller('scoped-controller/') 47 | class ScopedController { 48 | @Get() 49 | public returnSomethingThatMatters(): string | null { 50 | return this.child1.somethingThatMatters; 51 | } 52 | constructor( 53 | @Inject('SCOPED_TOKEN') public child1: ScopedInjectableInterface, 54 | ) { 55 | } 56 | } 57 | 58 | @Module({ 59 | controllers: [ScopedController, SideEffectController], 60 | injectables: [ 61 | { 62 | token: 'SCOPED_TOKEN', 63 | useClass: ScopedInjectable, 64 | }, 65 | InjectableUsingScoped, 66 | ], 67 | }) 68 | class ParentAfterScopedModule {} 69 | 70 | await testContext.step( 71 | 'handleRequest is called before request when defined in a scoped service', 72 | async () => { 73 | const app = new DanetApplication(); 74 | await app.init(ParentAfterScopedModule); 75 | const listenEvent = await app.listen(0); 76 | 77 | const res = await fetch( 78 | `http://localhost:${listenEvent.port}/scoped-controller/`, 79 | { 80 | method: 'GET', 81 | }, 82 | ); 83 | const text = await res.text(); 84 | assertEquals(text, `Received a GET request`); 85 | await app.close(); 86 | }, 87 | ); 88 | 89 | await testContext.step( 90 | 'handleRequest is called before request when defined in a scoped service but thats a side effect', 91 | async () => { 92 | const app = new DanetApplication(); 93 | await app.init(ParentAfterScopedModule); 94 | const listenEvent = await app.listen(0); 95 | 96 | const res = await fetch( 97 | `http://localhost:${listenEvent.port}/side-effect/`, 98 | { 99 | method: 'GET', 100 | }, 101 | ); 102 | const text = await res.text(); 103 | assertEquals(text, `Received a GET request`); 104 | await app.close(); 105 | }, 106 | ); 107 | }); 108 | -------------------------------------------------------------------------------- /src/guard/executor.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException } from '../exception/http/mod.ts'; 2 | import { Injector } from '../injector/injector.ts'; 3 | import { MetadataHelper } from '../metadata/helper.ts'; 4 | import { ControllerConstructor } from '../router/controller/constructor.ts'; 5 | import { Callback, ExecutionContext, HttpContext } from '../router/router.ts'; 6 | import { Constructor } from '../utils/constructor.ts'; 7 | import { GLOBAL_GUARD } from './constants.ts'; 8 | import { guardMetadataKey } from './decorator.ts'; 9 | import { AuthGuard } from './interface.ts'; 10 | 11 | /** 12 | * Responsible for executing various guards in a given execution context. 13 | * It handles the execution of global guards, controller-level guards, and method-level guards. 14 | * https://danet.land/overview/guards.html 15 | * @constructor 16 | * @param {Injector} injector - The injector instance used to retrieve and manage dependencies. 17 | */ 18 | export class GuardExecutor { 19 | constructor(private injector: Injector) { 20 | } 21 | 22 | /** 23 | * https://danet.land/overview/guards.html 24 | * Executes all relevant guards for the given context, controller, and controller method. 25 | * 26 | * This method first executes the global guard, followed by the controller and method-specific 27 | * authentication guards. 28 | * 29 | * @param context - The execution context which provides details about the current request. 30 | * @param Controller - The constructor of the controller being executed. 31 | * @param ControllerMethod - The method of the controller being executed. 32 | * @returns A promise that resolves when all relevant guards have been executed. 33 | */ 34 | async executeAllRelevantGuards( 35 | context: ExecutionContext, 36 | Controller: ControllerConstructor, 37 | ControllerMethod: Callback, 38 | ) { 39 | await this.executeGlobalGuard(context); 40 | await this.executeControllerAndMethodAuthGuards( 41 | context, 42 | Controller, 43 | ControllerMethod, 44 | ); 45 | } 46 | 47 | private async executeGlobalGuard(context: ExecutionContext) { 48 | if (this.injector.has(GLOBAL_GUARD)) { 49 | const globalGuard: AuthGuard = await this.injector.get(GLOBAL_GUARD); 50 | await this.executeGuard(globalGuard, context); 51 | } 52 | } 53 | 54 | private async executeControllerAndMethodAuthGuards( 55 | context: ExecutionContext, 56 | Controller: ControllerConstructor, 57 | ControllerMethod: Callback, 58 | ) { 59 | await this.executeGuardFromMetadata(context, Controller); 60 | await this.executeGuardFromMetadata(context, ControllerMethod); 61 | } 62 | 63 | private async executeGuard(guard: AuthGuard, context: ExecutionContext) { 64 | if (guard) { 65 | const canActivate = await guard.canActivate(context); 66 | if (!canActivate) { 67 | throw new ForbiddenException(); 68 | } 69 | } 70 | } 71 | 72 | private async executeGuardFromMetadata( 73 | context: ExecutionContext, 74 | // deno-lint-ignore ban-types 75 | constructor: Constructor | Function, 76 | ) { 77 | const GuardConstructor: Constructor = MetadataHelper.getMetadata< 78 | Constructor 79 | >( 80 | guardMetadataKey, 81 | constructor, 82 | ); 83 | if (GuardConstructor) { 84 | await this.injector.registerInjectables([GuardConstructor]); 85 | const guardInstance: AuthGuard = this.injector.get( 86 | GuardConstructor, 87 | ); 88 | await this.executeGuard(guardInstance, context); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/events/events.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../mod.ts'; 2 | 3 | // deno-lint-ignore no-explicit-any 4 | type Listener

= (payload: P) => void; 5 | 6 | /** 7 | * Provides event-driven architecture for subscribing, emitting, and unsubscribing events. 8 | * 9 | * @example 10 | * ```ts 11 | * const emitter = new EventEmitter(); 12 | * 13 | * // Subscribe to an event 14 | * emitter.subscribe('eventName', (payload) => { 15 | * console.log(payload); 16 | * }); 17 | * 18 | * // Emit an event 19 | * emitter.emit('eventName', { key: 'value' }); 20 | * 21 | * // Unsubscribe from an event 22 | * emitter.unsubscribe('eventName'); 23 | * ``` 24 | */ 25 | export class EventEmitter { 26 | private logger: Logger = new Logger('EventEmitter'); 27 | private listenersRegistered: Map; 28 | private eventTarget: EventTarget; 29 | 30 | constructor() { 31 | this.listenersRegistered = new Map(); 32 | this.eventTarget = new EventTarget(); 33 | } 34 | 35 | /** 36 | * Emits an event to a specified channel with the given payload. 37 | * 38 | * @template P - The type of the payload. 39 | * @param {string} channelName - The name of the channel to emit the event to. 40 | * @param {P} payload - The payload to send with the event. 41 | * @throws {Error} If there is no listener registered for the specified channel. 42 | * @returns {void} 43 | */ 44 | emit

(channelName: string, payload: P): void { 45 | const channels = Array.from(this.listenersRegistered.keys()); 46 | if (!channels.includes(channelName)) { 47 | throw new Error(`No listener for '${channelName}' channel`); 48 | } 49 | 50 | const event = new CustomEvent(channelName, { detail: payload }); 51 | this.eventTarget.dispatchEvent(event); 52 | 53 | this.logger.log( 54 | `event send to '${channelName}' channel`, 55 | ); 56 | } 57 | 58 | /** 59 | * Subscribes a listener to a specified event channel. 60 | * 61 | * @template P - The type of the payload expected by the listener. 62 | * @param {string} channelName - The name of the event channel to subscribe to. 63 | * @param {Listener

} listener - The listener function to be called when an event is emitted on the specified channel. 64 | * @returns {void} 65 | */ 66 | subscribe

(channelName: string, listener: Listener

): void { 67 | const eventListener = (ev: Event) => { 68 | const { detail: payload } = ev as CustomEvent; 69 | return listener(payload); 70 | }; 71 | this.eventTarget.addEventListener(channelName, eventListener); 72 | 73 | const listeners = this.listenersRegistered.get(channelName) ?? []; 74 | this.listenersRegistered.set(channelName, [...listeners, eventListener]); 75 | 76 | this.logger.log( 77 | `event listener subscribed to '${channelName}' channel`, 78 | ); 79 | } 80 | 81 | /** 82 | * Unsubscribes from event listeners for a specific channel or all channels. 83 | * 84 | * @param channelName - The name of the channel to unsubscribe from. If not provided, unsubscribes from all channels. 85 | * @returns void 86 | */ 87 | unsubscribe(channelName?: string): void { 88 | this.logger.log( 89 | `cleaning up event listeners for '${channelName ?? 'all'}' channel`, 90 | ); 91 | 92 | if (channelName) { 93 | return this.deleteChannel(channelName); 94 | } 95 | 96 | for (const channel of this.listenersRegistered.keys()) { 97 | this.deleteChannel(channel); 98 | } 99 | } 100 | 101 | private deleteChannel(channelName: string) { 102 | const listeners = this.listenersRegistered.get(channelName) ?? []; 103 | 104 | listeners.map((listener) => 105 | this.eventTarget.removeEventListener(channelName, listener) 106 | ); 107 | 108 | this.listenersRegistered.delete(channelName); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/router/middleware/executor.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '../../injector/injector.ts'; 2 | import { Callback, ExecutionContext, HttpContext } from '../router.ts'; 3 | import { ControllerConstructor } from '../controller/constructor.ts'; 4 | import { InjectableConstructor } from '../../injector/injectable/constructor.ts'; 5 | import { MetadataHelper } from '../../metadata/helper.ts'; 6 | import { 7 | DanetMiddleware, 8 | isMiddlewareClass, 9 | MiddlewareFunction, 10 | middlewareMetadataKey, 11 | NextFunction, 12 | } from './decorator.ts'; 13 | import { Constructor } from '../../utils/constructor.ts'; 14 | import { globalMiddlewareContainer } from './global-container.ts'; 15 | 16 | /** 17 | * The `MiddlewareExecutor` class is responsible for executing a series of middleware functions 18 | * in a specified order. It utilizes an injector to manage dependencies and ensures that all 19 | * relevant middlewares are executed for a given context, controller, and controller method. 20 | */ 21 | export class MiddlewareExecutor { 22 | /** 23 | * Constructs a new `MiddlewareExecutor` instance. 24 | * 25 | * @param injector - The injector used to manage dependencies. 26 | */ 27 | constructor(private injector: Injector) {} 28 | 29 | /** 30 | * Executes all relevant middlewares for the given context, controller, and controller method. 31 | * 32 | * @param context - The execution context. 33 | * @param Controller - The controller constructor. 34 | * @param ControllerMethod - The controller method callback. 35 | * @param next - The next function to be called after all middlewares have been executed. 36 | * @returns A promise that resolves to the result of the next function or the middleware chain. 37 | */ 38 | async executeAllRelevantMiddlewares( 39 | context: ExecutionContext, 40 | Controller: ControllerConstructor, 41 | ControllerMethod: Callback, 42 | next: NextFunction, 43 | ): Promise { 44 | const middlewares = [...globalMiddlewareContainer]; 45 | middlewares.push(...this.getSymbolMiddlewares(Controller)); 46 | middlewares.push(...this.getSymbolMiddlewares(ControllerMethod)); 47 | 48 | if (middlewares.length === 0) return next(); 49 | const injectablesMiddleware: InjectableConstructor[] = middlewares.filter( 50 | isMiddlewareClass, 51 | ) as InjectableConstructor[]; 52 | let index = -1; 53 | await this.injector.registerInjectables(injectablesMiddleware); 54 | // deno-lint-ignore no-explicit-any 55 | const dispatch = async (i: number): Promise => { 56 | if (i <= index) { 57 | throw new Error('next() called multiple times.'); 58 | } 59 | index = i; 60 | let fn; 61 | if (i === middlewares.length) { 62 | fn = next; 63 | return fn(); 64 | } 65 | const currentMiddleware = middlewares[i]; 66 | if (isMiddlewareClass(currentMiddleware)) { 67 | const middlewareInstance: DanetMiddleware = this.injector.get< 68 | DanetMiddleware 69 | >(currentMiddleware as Constructor); 70 | fn = async (ctx: ExecutionContext, nextFn: NextFunction) => { 71 | return await middlewareInstance.action(ctx, nextFn); 72 | }; 73 | } else { 74 | fn = async (ctx: ExecutionContext, nextFn: NextFunction) => { 75 | return await (currentMiddleware as MiddlewareFunction)(ctx, nextFn); 76 | }; 77 | } 78 | if (!fn) { 79 | return; 80 | } 81 | return await fn(context, dispatch.bind(null, i + 1)); 82 | }; 83 | 84 | return dispatch(0); 85 | } 86 | 87 | private getSymbolMiddlewares( 88 | symbol: unknown, 89 | ) { 90 | const middlewares: InjectableConstructor[] = MetadataHelper.getMetadata( 91 | middlewareMetadataKey, 92 | symbol, 93 | ); 94 | if (middlewares) { 95 | return middlewares; 96 | } 97 | return []; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /spec/websocket/exception-filter.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../../src/deps_test.ts'; 2 | import { DanetApplication } from '../../src/app.ts'; 3 | import { Catch, UseFilter } from '../../src/exception/filter/decorator.ts'; 4 | import { ExceptionFilter } from '../../src/exception/filter/interface.ts'; 5 | import { Module } from '../../src/module/decorator.ts'; 6 | import { Get } from '../../src/router/controller/decorator.ts'; 7 | import { HttpContext } from '../../src/router/router.ts'; 8 | import { Injectable } from '../../src/injector/injectable/decorator.ts'; 9 | import { 10 | OnWebSocketMessage, 11 | WebSocketController, 12 | } from '../../src/router/websocket/decorator.ts'; 13 | 14 | @Injectable() 15 | class SimpleService { 16 | private a = 0; 17 | doSomething() { 18 | this.a++; 19 | } 20 | } 21 | 22 | class CustomException extends Error { 23 | public customField = 'i am a custom field'; 24 | constructor(text: string) { 25 | super(text); 26 | } 27 | } 28 | 29 | @Injectable() 30 | class ErrorFilter implements ExceptionFilter { 31 | constructor(private simpleService: SimpleService) { 32 | } 33 | 34 | catch(exception: any, context: HttpContext) { 35 | this.simpleService.doSomething(); 36 | return { topic: 'catch-all', data: 'nicely-done' }; 37 | } 38 | } 39 | 40 | @Injectable() 41 | @Catch(CustomException) 42 | class CustomErrorFilter implements ExceptionFilter { 43 | constructor(private simpleService: SimpleService) { 44 | } 45 | 46 | catch(exception: any, context: HttpContext) { 47 | this.simpleService.doSomething(); 48 | return { topic: 'catch-custom', data: 'nicely-done' }; 49 | } 50 | } 51 | 52 | @UseFilter(ErrorFilter) 53 | @WebSocketController('catch-all') 54 | class ControllerWithFilter { 55 | @OnWebSocketMessage('trigger') 56 | simpleGet() { 57 | throw Error('an error'); 58 | } 59 | } 60 | 61 | @WebSocketController('custom-error') 62 | class ControllerWithCustomFilter { 63 | @UseFilter(CustomErrorFilter) 64 | @OnWebSocketMessage('trigger') 65 | customError() { 66 | throw new CustomException('an error'); 67 | } 68 | } 69 | @Module({ 70 | controllers: [ControllerWithFilter, ControllerWithCustomFilter], 71 | injectables: [SimpleService], 72 | }) 73 | class ModuleWithFilter {} 74 | 75 | Deno.test('Catch all', async () => { 76 | return new Promise(async (resolve) => { 77 | const app = new DanetApplication(); 78 | await app.init(ModuleWithFilter); 79 | const listenEvent = await app.listen(0); 80 | 81 | const websocket = new WebSocket( 82 | `ws://localhost:${listenEvent.port}/catch-all`, 83 | ); 84 | websocket.onmessage = async (e) => { 85 | const parsedResponse = JSON.parse(e.data); 86 | assertEquals(parsedResponse.topic, 'catch-all'); 87 | assertEquals(parsedResponse.data, 'nicely-done'); 88 | await app.close(); 89 | websocket.close(); 90 | resolve(); 91 | }; 92 | websocket.onopen = (e) => { 93 | websocket.send( 94 | JSON.stringify({ topic: 'trigger', data: { didIGetThisBack: true } }), 95 | ); 96 | }; 97 | }); 98 | }); 99 | 100 | Deno.test('Catch custom', async () => { 101 | return new Promise(async (resolve) => { 102 | const app = new DanetApplication(); 103 | await app.init(ModuleWithFilter); 104 | const listenEvent = await app.listen(0); 105 | 106 | const websocket = new WebSocket( 107 | `ws://localhost:${listenEvent.port}/custom-error`, 108 | ); 109 | websocket.onmessage = async (e) => { 110 | const parsedResponse = JSON.parse(e.data); 111 | assertEquals(parsedResponse.topic, 'catch-custom'); 112 | assertEquals(parsedResponse.data, 'nicely-done'); 113 | await app.close(); 114 | websocket.close(); 115 | resolve(); 116 | }; 117 | websocket.onopen = (e) => { 118 | websocket.send( 119 | JSON.stringify({ topic: 'trigger', data: { didIGetThisBack: true } }), 120 | ); 121 | }; 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/schedule/module.ts: -------------------------------------------------------------------------------- 1 | import { OnAppBootstrap, OnAppClose } from '../hook/interfaces.ts'; 2 | import { MetadataHelper } from '../metadata/helper.ts'; 3 | import { InjectableConstructor, injector, Logger, Module } from '../mod.ts'; 4 | import { 5 | intervalMetadataKey, 6 | scheduleMetadataKey, 7 | timeoutMetadataKey, 8 | } from './constants.ts'; 9 | import { 10 | CronMetadataPayload, 11 | IntervalMetadataPayload, 12 | TimeoutMetadataPayload, 13 | } from './types.ts'; 14 | 15 | /* Use this module if you want to run CRON https://danet.land/techniques/task-scheduling.html */ 16 | 17 | @Module({}) 18 | export class ScheduleModule implements OnAppBootstrap, OnAppClose { 19 | private logger: Logger = new Logger('ScheduleModule'); 20 | private abortController = new AbortController(); 21 | private intervalSet = new Set(); 22 | private timeoutSet = new Set(); 23 | 24 | onAppBootstrap() { 25 | for (const types of injector.injectables) { 26 | this.registerAvailableEventListeners(types); 27 | } 28 | } 29 | 30 | onAppClose() { 31 | this.logger.log(`Cleaning up all scheduled events`); 32 | this.abortController.abort(); 33 | 34 | for (const intervalId of this.intervalSet) { 35 | clearInterval(intervalId); 36 | } 37 | 38 | for (const timeoutId of this.timeoutSet) { 39 | clearTimeout(timeoutId); 40 | } 41 | } 42 | 43 | private registerAvailableEventListeners(Type: InjectableConstructor) { 44 | const methods = Object.getOwnPropertyNames(Type.constructor.prototype); 45 | 46 | for (const method of methods) { 47 | this.registerCronJobs(Type, method); 48 | this.registerIntervals(Type, method); 49 | this.registerTimeouts(Type, method); 50 | } 51 | } 52 | 53 | private registerTimeouts( 54 | injectableInstance: InjectableConstructor, 55 | method: string, 56 | ) { 57 | this.registerTimedEvents( 58 | timeoutMetadataKey, 59 | injectableInstance, 60 | method, 61 | ({ timeout }, callback) => { 62 | this.logger.log( 63 | `Scheduling '${method}' to run as a timeout callback`, 64 | ); 65 | 66 | this.timeoutSet.add(setTimeout(callback, timeout)); 67 | }, 68 | ); 69 | } 70 | 71 | private registerIntervals( 72 | injectableInstance: InjectableConstructor, 73 | method: string, 74 | ) { 75 | this.registerTimedEvents( 76 | intervalMetadataKey, 77 | injectableInstance, 78 | method, 79 | ({ interval }, callback) => { 80 | this.logger.log( 81 | `Scheduling '${method}' to run as a interval callback`, 82 | ); 83 | 84 | this.intervalSet.add(setInterval(callback, interval)); 85 | }, 86 | ); 87 | } 88 | 89 | private registerCronJobs( 90 | injectableInstance: InjectableConstructor, 91 | method: string, 92 | ) { 93 | this.registerTimedEvents( 94 | scheduleMetadataKey, 95 | injectableInstance, 96 | method, 97 | ({ cron }, callback) => { 98 | this.logger.log(`Scheduling '${method}' to run as a cron job`); 99 | Deno.cron(method, cron, this.abortController, callback); 100 | }, 101 | ); 102 | } 103 | 104 | private registerTimedEvents( 105 | metadataKey: string, 106 | injectableInstance: InjectableConstructor, 107 | method: string, 108 | handler: (metadata: T, cb: () => void) => void, 109 | ) { 110 | const target = injectableInstance.constructor.prototype[method]; 111 | const metadata = MetadataHelper.getMetadata(metadataKey, target); 112 | if (!metadata) return; 113 | 114 | const callback = this.makeCallbackWithScope(injectableInstance, target); 115 | handler(metadata, callback); 116 | } 117 | 118 | // Function to ensures we don't lose `this` scope from target 119 | // deno-lint-ignore no-explicit-any 120 | private makeCallbackWithScope(instance: any, target: any) { 121 | return () => instance[target.name](); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/exception/filter/executor.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../../metadata/helper.ts'; 2 | import { ControllerConstructor } from '../../router/controller/constructor.ts'; 3 | import { Callback, HttpContext } from '../../router/router.ts'; 4 | import { Constructor } from '../../utils/constructor.ts'; 5 | import { 6 | filterCatchTypeMetadataKey, 7 | filterExceptionMetadataKey, 8 | } from './decorator.ts'; 9 | import { ExceptionFilter } from './interface.ts'; 10 | import { Injector } from '../../injector/injector.ts'; 11 | import { WebSocketPayload } from '../../router/websocket/payload.ts'; 12 | import { globalExceptionFilterContainer } from './global-container.ts'; 13 | 14 | /** 15 | * Responsible for executing exception filters 16 | * based on metadata and handling errors within the context of an HTTP request. 17 | * It utilizes the injector to manage dependencies and retrieve filter instances. 18 | * 19 | * @constructor 20 | * @param {Injector} injector - The dependency injector used to manage and retrieve filter instances. 21 | */ 22 | export class FilterExecutor { 23 | constructor(private injector: Injector) { 24 | } 25 | 26 | private getErrorTypeCaughtByExceptionFilter( 27 | exceptionConstructor: Constructor, 28 | ) { 29 | // deno-lint-ignore ban-types 30 | return MetadataHelper.getMetadata( 31 | filterCatchTypeMetadataKey, 32 | exceptionConstructor, 33 | ); 34 | } 35 | 36 | private executeFilter( 37 | exceptionFilter: ExceptionFilter, 38 | context: HttpContext, 39 | error: unknown, 40 | ): Response | undefined | { topic: string; data: unknown } { 41 | if (exceptionFilter) { 42 | const errorTypeCaughtByFilter = this.getErrorTypeCaughtByExceptionFilter( 43 | // deno-lint-ignore no-explicit-any 44 | (exceptionFilter as any).constructor, 45 | ); 46 | if (errorTypeCaughtByFilter) { 47 | if (!(error instanceof errorTypeCaughtByFilter)) { 48 | return; 49 | } 50 | } 51 | return exceptionFilter.catch(error, context); 52 | } 53 | return; 54 | } 55 | 56 | private async executeFilterFromMetadata( 57 | context: HttpContext, 58 | error: unknown, 59 | // deno-lint-ignore ban-types 60 | constructor: Constructor | Function, 61 | ): Promise { 62 | const FilterConstructor: Constructor = MetadataHelper 63 | .getMetadata>( 64 | filterExceptionMetadataKey, 65 | constructor, 66 | ); 67 | if (FilterConstructor) { 68 | await this.injector.registerInjectables([FilterConstructor]); 69 | const filter: ExceptionFilter = this.injector.get( 70 | FilterConstructor, 71 | ); 72 | return this.executeFilter(filter, context, error); 73 | } 74 | return; 75 | } 76 | 77 | /** 78 | * Executes filters for both the controller and the controller method. 79 | * 80 | * @param context - The HTTP context for the current request. 81 | * @param error - The error that occurred. 82 | * @param Controller - The constructor of the controller. 83 | * @param ControllerMethod - The method of the controller to be executed. 84 | * @returns A promise that resolves to a `Response`, `undefined`, or `WebSocketPayload`. 85 | */ 86 | async executeControllerAndMethodFilter( 87 | context: HttpContext, 88 | error: unknown, 89 | Controller: ControllerConstructor, 90 | ControllerMethod: Callback, 91 | ): Promise { 92 | let response: Response | undefined | WebSocketPayload; 93 | response = await this.executeFilterFromMetadata(context, error, Controller); 94 | 95 | if (response) return response; 96 | 97 | response = await this.executeFilterFromMetadata( 98 | context, 99 | error, 100 | ControllerMethod, 101 | ); 102 | if (response) return response; 103 | 104 | for (const filter of globalExceptionFilterContainer) { 105 | response = this.executeFilter(filter, context, error); 106 | if (response) return response; 107 | } 108 | 109 | return; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /spec/validate-body-decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertExists } from '@std/testing/asserts'; 2 | import { DanetApplication } from '../src/mod.ts'; 3 | import { Module } from '../src/module/mod.ts'; 4 | import { Body, Controller, Get, Post } from '../src/router/controller/mod.ts'; 5 | import { IsNumber, IsString, LengthGreater } from '../validation.ts'; 6 | 7 | // Utils --------------- 8 | function jsonWithMessage(msg: string) { 9 | return ({ message: msg }); 10 | } 11 | async function fetchWithBody(url: string, body: any) { 12 | return fetch(url, { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify(body), 18 | }); 19 | } 20 | // --------------------- 21 | 22 | class DTO { 23 | @IsString() 24 | @LengthGreater(20) 25 | name!: string; 26 | 27 | @IsNumber() 28 | age!: number; 29 | } 30 | 31 | @Controller('/test') 32 | class AppController { 33 | constructor() {} 34 | 35 | @Get('') 36 | justSayHello() { 37 | return jsonWithMessage('Hello'); 38 | } 39 | 40 | @Post('') 41 | sayHelloToHim1(@Body() body: DTO) { 42 | return jsonWithMessage(`Hello ${body.name}`); 43 | } 44 | 45 | @Post('partial-body') 46 | sayHelloToName(@Body('name') name: string) { 47 | return jsonWithMessage(`Hello ${name}`); 48 | } 49 | 50 | @Post('validate-only-prop') 51 | sayHelloToPerson(@Body('person') person: DTO) { 52 | return jsonWithMessage(`Hello ${person.name}`); 53 | } 54 | } 55 | 56 | @Module({ 57 | controllers: [AppController], 58 | }) 59 | class App {} 60 | 61 | Deno.test('Controller works', async () => { 62 | const app = new DanetApplication(); 63 | await app.init(App); 64 | const port = (await app.listen(0)).port; 65 | 66 | const res = await fetch(`http://localhost:${port}/test`, { 67 | method: 'GET', 68 | }); 69 | const json = await res.json(); 70 | assertEquals(json, jsonWithMessage('Hello')); 71 | await app.close(); 72 | }); 73 | 74 | Deno.test('Return 200 if body follows DTO', async (t) => { 75 | const app = new DanetApplication(); 76 | await app.init(App); 77 | const port = (await app.listen(0)).port; 78 | let res, json; 79 | 80 | res = await fetchWithBody(`http://localhost:${port}/test`, { 81 | name: 'James Very Long Name wow Awesome', 82 | age: 23, // A string as number 83 | }); 84 | assertEquals(res.status, 200); 85 | await res.body?.cancel(); 86 | 87 | await app.close(); 88 | }); 89 | 90 | Deno.test('Return 200 when using partial body decorator', async (t) => { 91 | const app = new DanetApplication(); 92 | await app.init(App); 93 | const port = (await app.listen(0)).port; 94 | let res, json; 95 | 96 | res = await fetchWithBody(`http://localhost:${port}/test/partial-body`, { 97 | name: 'James', 98 | age: 23, // A string as number 99 | }); 100 | assertEquals(res.status, 200); 101 | await res.body?.cancel(); 102 | 103 | await app.close(); 104 | }); 105 | 106 | Deno.test('Return 200 when prop is a class with validators and valid', async (t) => { 107 | const app = new DanetApplication(); 108 | await app.init(App); 109 | const port = (await app.listen(0)).port; 110 | let res, json; 111 | 112 | res = await fetchWithBody( 113 | `http://localhost:${port}/test/validate-only-prop`, 114 | { 115 | person: { 116 | name: 'James Has A Bery Long Name Be Ready', 117 | age: 23, // A string as number 118 | }, 119 | }, 120 | ); 121 | assertEquals(res.status, 200); 122 | await res.body?.cancel(); 123 | 124 | await app.close(); 125 | }); 126 | 127 | Deno.test('Return 400 if body is NOT following DTO', async (t) => { 128 | const app = new DanetApplication(); 129 | await app.init(App); 130 | const port = (await app.listen(0)).port; 131 | let res, json; 132 | 133 | res = await fetchWithBody(`http://localhost:${port}/test`, { 134 | name: 'James', 135 | age: 23, 136 | }); 137 | assertEquals(res.status, 400); 138 | 139 | // Json exist 140 | json = await res.json(); 141 | assertExists(json); 142 | assertExists(json.reasons); 143 | 144 | res = await fetchWithBody(`http://localhost:${port}/test`, { 145 | name: 'James', 146 | age: '23', // A string as number 147 | }); 148 | assertEquals(res.status, 400); 149 | 150 | // Json exist 151 | json = await res.json(); 152 | assertExists(json); 153 | assertExists(json.reasons); 154 | 155 | await app.close(); 156 | }); 157 | -------------------------------------------------------------------------------- /spec/events.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | DanetApplication, 4 | EventEmitter, 5 | EventEmitterModule, 6 | Get, 7 | Module, 8 | OnEvent, 9 | } from '../mod.ts'; 10 | import { 11 | assertEquals, 12 | assertSpyCall, 13 | assertThrows, 14 | spy, 15 | } from '../src/deps_test.ts'; 16 | 17 | Deno.test('EventEmitter Service', async (t) => { 18 | await t.step('subscribe multiple listeners for the same topic', async () => { 19 | const emitter = new EventEmitter(); 20 | const fn1 = spy(() => {}); 21 | const fn2 = spy(() => {}); 22 | 23 | emitter.subscribe('test', fn1); 24 | emitter.subscribe('test', fn2); 25 | 26 | emitter.emit('test', 'something'); 27 | 28 | assertSpyCall(fn1, 0, { 29 | args: ['something'], 30 | returned: undefined, 31 | }); 32 | 33 | assertSpyCall(fn2, 0, { 34 | args: ['something'], 35 | returned: undefined, 36 | }); 37 | 38 | emitter.unsubscribe(); 39 | }); 40 | 41 | await t.step('subscribe listeners for the multiple topics', async () => { 42 | const emitter = new EventEmitter(); 43 | const fn1 = spy(() => {}); 44 | const fn2 = spy(() => {}); 45 | 46 | emitter.subscribe('test', fn1); 47 | emitter.subscribe('test-2', fn2); 48 | 49 | emitter.emit('test', 'something'); 50 | 51 | assertSpyCall(fn1, 0, { 52 | args: ['something'], 53 | returned: undefined, 54 | }); 55 | 56 | assertEquals(fn2.calls.length, 0); 57 | 58 | emitter.emit('test-2', 'something'); 59 | 60 | assertSpyCall(fn2, 0, { 61 | args: ['something'], 62 | returned: undefined, 63 | }); 64 | 65 | assertEquals(fn1.calls.length, 1); 66 | 67 | emitter.unsubscribe(); 68 | }); 69 | 70 | await t.step('throw error if emit an event with no listener', async () => { 71 | const emitter = new EventEmitter(); 72 | const fn1 = spy(() => {}); 73 | 74 | assertThrows(() => emitter.emit('test', 'something')); 75 | 76 | emitter.subscribe('test', fn1); 77 | 78 | assertEquals(fn1.calls.length, 0); 79 | 80 | emitter.emit('test', 'something'); 81 | 82 | assertSpyCall(fn1, 0, { 83 | args: ['something'], 84 | returned: undefined, 85 | }); 86 | 87 | emitter.unsubscribe(); 88 | }); 89 | 90 | await t.step('throw error if emit to a unsubscribed topic', async () => { 91 | const emitter = new EventEmitter(); 92 | const fn1 = spy(() => {}); 93 | 94 | assertEquals(fn1.calls.length, 0); 95 | 96 | emitter.subscribe('test', fn1); 97 | emitter.emit('test', 'something'); 98 | 99 | assertSpyCall(fn1, 0, { 100 | args: ['something'], 101 | returned: undefined, 102 | }); 103 | 104 | emitter.unsubscribe('test'); 105 | 106 | assertThrows(() => emitter.emit('test', 'something')); 107 | assertEquals(fn1.calls.length, 1); 108 | 109 | emitter.unsubscribe(); 110 | }); 111 | }); 112 | 113 | Deno.test('EventEmitter Module', async (t) => { 114 | const callback = spy((_payload: any) => {}); 115 | const payload = { name: 'test' }; 116 | 117 | class TestListener { 118 | public somethingDone = 0; 119 | private doSomething() { 120 | this.somethingDone++; 121 | } 122 | @OnEvent('trigger') 123 | getSomething(payload: any) { 124 | callback(payload); 125 | this.doSomething(); 126 | } 127 | } 128 | 129 | @Controller('trigger') 130 | class TestController { 131 | constructor(private emitter: EventEmitter) {} 132 | 133 | @Get() 134 | getSomething() { 135 | this.emitter.emit('trigger', payload); 136 | return 'OK'; 137 | } 138 | } 139 | 140 | @Module({ 141 | imports: [EventEmitterModule], 142 | controllers: [TestController], 143 | injectables: [TestListener], 144 | }) 145 | class TestModule {} 146 | 147 | const application = new DanetApplication(); 148 | await application.init(TestModule); 149 | const listenerInfo = await application.listen(0); 150 | 151 | await t.step('validate if api call trigger event', async () => { 152 | assertEquals(callback.calls.length, 0); 153 | 154 | let res = await fetch(`http://localhost:${listenerInfo.port}/trigger`); 155 | 156 | assertEquals(res.status, 200); 157 | assertEquals(await res.text(), 'OK'); 158 | assertEquals(callback.calls.length, 1); 159 | assertSpyCall(callback, 0, { 160 | args: [payload], 161 | }); 162 | 163 | res = await fetch(`http://localhost:${listenerInfo.port}/trigger`); 164 | 165 | assertEquals(res.status, 200); 166 | assertEquals(await res.text(), 'OK'); 167 | assertEquals(callback.calls.length, 2); 168 | }); 169 | 170 | await application.close(); 171 | }); 172 | -------------------------------------------------------------------------------- /spec/exception-filter.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Catch, UseFilter } from '../src/exception/filter/decorator.ts'; 4 | import { ExceptionFilter } from '../src/exception/filter/interface.ts'; 5 | import { Module } from '../src/module/decorator.ts'; 6 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 7 | import { HttpContext } from '../src/router/router.ts'; 8 | import { Injectable } from '../src/injector/injectable/decorator.ts'; 9 | 10 | @Injectable() 11 | class SimpleService { 12 | private a = 0; 13 | doSomething() { 14 | this.a++; 15 | } 16 | } 17 | 18 | class CustomException extends Error { 19 | public customField = 'i am a custom field'; 20 | constructor(text: string) { 21 | super(text); 22 | } 23 | } 24 | 25 | @Injectable() 26 | class ErrorFilter implements ExceptionFilter { 27 | constructor(private simpleService: SimpleService) { 28 | } 29 | 30 | catch(exception: any, context: HttpContext) { 31 | this.simpleService.doSomething(); 32 | return context.newResponse(JSON.stringify({ 33 | wePassedInFilterCatchingAllErrors: true, 34 | }), 401); 35 | } 36 | } 37 | 38 | @Injectable() 39 | @Catch(CustomException) 40 | class CustomErrorFilter implements ExceptionFilter { 41 | constructor(private simpleService: SimpleService) { 42 | } 43 | 44 | catch(exception: any, context: HttpContext) { 45 | this.simpleService.doSomething(); 46 | return context.json({ 47 | wePassedInFilterCatchingOnlySomeError: true, 48 | }); 49 | } 50 | } 51 | 52 | @UseFilter(ErrorFilter) 53 | @Controller('') 54 | class ControllerWithFilter { 55 | @Get('/') 56 | simpleGet() { 57 | throw Error('an error'); 58 | } 59 | } 60 | 61 | @Controller('custom-error') 62 | class ControllerWithCustomFilter { 63 | @UseFilter(CustomErrorFilter) 64 | @Get('') 65 | customError() { 66 | throw new CustomException('an error'); 67 | } 68 | 69 | @Get('unexpected-error') 70 | unexpectedError() { 71 | throw Error('unexpected'); 72 | } 73 | } 74 | @Module({ 75 | controllers: [ControllerWithFilter, ControllerWithCustomFilter], 76 | injectables: [SimpleService], 77 | }) 78 | class ModuleWithFilter {} 79 | 80 | for ( 81 | const testName of [ 82 | 'Exception Filter with @Catch catch related errors', 83 | 'Method exception filter works', 84 | ] 85 | ) { 86 | Deno.test(testName, async () => { 87 | const app = new DanetApplication(); 88 | await app.init(ModuleWithFilter); 89 | const listenEvent = await app.listen(0); 90 | 91 | const res = await fetch( 92 | `http://localhost:${listenEvent.port}/custom-error`, 93 | { 94 | method: 'GET', 95 | }, 96 | ); 97 | const json = await res.json(); 98 | assertEquals(json, { 99 | wePassedInFilterCatchingOnlySomeError: true, 100 | }); 101 | await app.close(); 102 | }); 103 | } 104 | 105 | Deno.test('Controller filter works', async () => { 106 | const app = new DanetApplication(); 107 | await app.init(ModuleWithFilter); 108 | const listenEvent = await app.listen(0); 109 | 110 | const res = await fetch(`http://localhost:${listenEvent.port}`, { 111 | method: 'GET', 112 | }); 113 | const json = await res.json(); 114 | assertEquals(res.status, 401); 115 | assertEquals(json, { 116 | wePassedInFilterCatchingAllErrors: true, 117 | }); 118 | await app.close(); 119 | }); 120 | 121 | Deno.test('throw 500 on unexpected error', async () => { 122 | const app = new DanetApplication(); 123 | await app.init(ModuleWithFilter); 124 | const listenEvent = await app.listen(0); 125 | 126 | const res = await fetch( 127 | `http://localhost:${listenEvent.port}/custom-error/unexpected-error`, 128 | { 129 | method: 'GET', 130 | }, 131 | ); 132 | assertEquals(500, res.status); 133 | await res.json(); 134 | await app.close(); 135 | }); 136 | 137 | 138 | Deno.test('global exception filter', async () => { 139 | const app = new DanetApplication(); 140 | await app.init(ModuleWithFilter); 141 | const simpleService = app.get(SimpleService); 142 | app.useGlobalExceptionFilter(new ErrorFilter(simpleService)); 143 | const listenEvent = await app.listen(0); 144 | 145 | const res = await fetch( 146 | `http://localhost:${listenEvent.port}/custom-error/unexpected-error`, 147 | { 148 | method: 'GET', 149 | }, 150 | ); 151 | assertEquals(res.status, 401); 152 | const json = await res.json(); 153 | assertEquals(json, { 154 | wePassedInFilterCatchingAllErrors: true, 155 | }); 156 | await app.close(); 157 | }); 158 | -------------------------------------------------------------------------------- /spec/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { Module } from '../src/module/decorator.ts'; 4 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 5 | import { Injectable } from '../src/injector/injectable/decorator.ts'; 6 | import { ExecutionContext, HttpContext } from '../src/router/router.ts'; 7 | import { 8 | DanetMiddleware, 9 | Middleware, 10 | NextFunction, 11 | } from '../src/router/middleware/decorator.ts'; 12 | import { BadRequestException } from '../src/exception/http/exceptions.ts'; 13 | 14 | @Injectable() 15 | class SimpleInjectable { 16 | doSomething() { 17 | return 'I did something'; 18 | } 19 | } 20 | 21 | @Injectable() 22 | class SimpleMiddleware implements DanetMiddleware { 23 | constructor(private simpleInjectable: SimpleInjectable) { 24 | } 25 | 26 | async action(ctx: ExecutionContext, next: NextFunction) { 27 | ctx.res.headers.append( 28 | 'middlewaredata', 29 | this.simpleInjectable.doSomething(), 30 | ); 31 | await next(); 32 | } 33 | } 34 | 35 | @Injectable() 36 | class ThrowingMiddleware implements DanetMiddleware { 37 | constructor(private simpleInjectable: SimpleInjectable) { 38 | } 39 | 40 | action(ctx: ExecutionContext) { 41 | throw new BadRequestException(); 42 | } 43 | } 44 | 45 | const secondMiddleware = async (ctx: HttpContext, next: NextFunction) => { 46 | if (!ctx.res) { 47 | ctx.res = new Response(); 48 | } 49 | ctx.res.headers.append('middlewaredata', ' ' + 'more'); 50 | await next(); 51 | }; 52 | 53 | @Controller('simple-controller') 54 | class SimpleController { 55 | @Get('/') 56 | @Middleware(SimpleMiddleware) 57 | getWithMiddleware() { 58 | return 'toto'; 59 | } 60 | 61 | @Get('/throwing') 62 | @Middleware(ThrowingMiddleware) 63 | getWithThrowing() { 64 | } 65 | } 66 | 67 | @Middleware(SimpleMiddleware, secondMiddleware) 68 | @Controller('controller-with-middleware') 69 | class ControllerWithMiddleware { 70 | @Get('/') 71 | getWithoutMiddleware() { 72 | } 73 | } 74 | 75 | @Module({ 76 | controllers: [SimpleController, ControllerWithMiddleware], 77 | injectables: [SimpleInjectable], 78 | }) 79 | class MyModule {} 80 | 81 | Deno.test('Middleware method decorator', async () => { 82 | const app = new DanetApplication(); 83 | await app.init(MyModule); 84 | const listenEvent = await app.listen(0); 85 | 86 | const res = await fetch( 87 | `http://localhost:${listenEvent.port}/simple-controller`, 88 | { 89 | method: 'GET', 90 | }, 91 | ); 92 | const text = res.headers.get('middlewaredata'); 93 | assertEquals(text, `I did something`); 94 | assertEquals(res.status, 200); 95 | assertEquals(await res.text(), 'toto'); 96 | await app.close(); 97 | }); 98 | 99 | Deno.test('Throwing middleware method decorator', async () => { 100 | const app = new DanetApplication(); 101 | await app.init(MyModule); 102 | const listenEvent = await app.listen(0); 103 | 104 | const res = await fetch( 105 | `http://localhost:${listenEvent.port}/simple-controller/throwing`, 106 | { 107 | method: 'GET', 108 | }, 109 | ); 110 | assertEquals(res.status, 400); 111 | await res.json(); 112 | await app.close(); 113 | }); 114 | 115 | Deno.test('Middleware controller decorator', async () => { 116 | const app = new DanetApplication(); 117 | await app.init(MyModule); 118 | const listenEvent = await app.listen(0); 119 | 120 | const res = await fetch( 121 | `http://localhost:${listenEvent.port}/controller-with-middleware`, 122 | { 123 | method: 'GET', 124 | }, 125 | ); 126 | //order is mixed up on purpose to check that argument order prevails 127 | const text = res.headers.get('middlewaredata'); 128 | assertEquals(res.status, 200); 129 | assertEquals(text, `I did something, more`); 130 | await res.body?.cancel(); 131 | await app.close(); 132 | }); 133 | 134 | @Injectable() 135 | class FirstGlobalMiddleware implements DanetMiddleware { 136 | async action(ctx: ExecutionContext, next: NextFunction) { 137 | ctx.res.headers.append('middlewaredata', '[first-middleware]'); 138 | return await next(); 139 | } 140 | } 141 | 142 | @Injectable() 143 | class SecondGlobalMiddleware implements DanetMiddleware { 144 | async action(ctx: ExecutionContext, next: NextFunction) { 145 | ctx.res.headers.append('middlewaredata', '[second-middleware]'); 146 | return await next(); 147 | } 148 | } 149 | 150 | Deno.test('Global middlewares', async () => { 151 | const app = new DanetApplication(); 152 | await app.init(MyModule); 153 | app.addGlobalMiddlewares(FirstGlobalMiddleware, SecondGlobalMiddleware); 154 | const listenEvent = await app.listen(0); 155 | 156 | const res = await fetch( 157 | `http://localhost:${listenEvent.port}/controller-with-middleware`, 158 | { 159 | method: 'GET', 160 | }, 161 | ); 162 | const text = res.headers.get('middlewaredata'); 163 | assertEquals(res.status, 200); 164 | assertEquals( 165 | text, 166 | `[first-middleware], [second-middleware], I did something, more`, 167 | ); 168 | await res.body?.cancel(); 169 | await app.close(); 170 | }); 171 | -------------------------------------------------------------------------------- /spec/auth-guard.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertObjectMatch } from '../src/deps_test.ts'; 2 | import { DanetApplication } from '../src/app.ts'; 3 | import { GLOBAL_GUARD } from '../src/guard/constants.ts'; 4 | import { UseGuard } from '../src/guard/decorator.ts'; 5 | import { AuthGuard } from '../src/guard/interface.ts'; 6 | import { Injectable } from '../src/injector/injectable/decorator.ts'; 7 | import { Module } from '../src/module/decorator.ts'; 8 | import { Controller, Get } from '../src/router/controller/decorator.ts'; 9 | import { ExecutionContext, HttpContext } from '../src/router/router.ts'; 10 | import { SetMetadata } from '../src/metadata/decorator.ts'; 11 | import { MetadataHelper } from '../src/metadata/helper.ts'; 12 | 13 | @Injectable() 14 | class SimpleService { 15 | private a = 0; 16 | doSomething() { 17 | this.a++; 18 | } 19 | } 20 | 21 | @Injectable() 22 | class GlobalGuard implements AuthGuard { 23 | constructor(private simpleService: SimpleService) { 24 | } 25 | 26 | canActivate(context: ExecutionContext) { 27 | this.simpleService.doSomething(); 28 | if (!context.res) { 29 | context.res = new Response(); 30 | } 31 | context.res.headers.append('passedInglobalGuard', 'true'); 32 | return true; 33 | } 34 | } 35 | 36 | @Injectable() 37 | class ControllerGuard implements AuthGuard { 38 | constructor(private simpleService: SimpleService) { 39 | } 40 | 41 | canActivate(context: ExecutionContext) { 42 | const controller = context.getClass(); 43 | const customMetadata = MetadataHelper.getMetadata( 44 | 'customMetadata', 45 | controller, 46 | ); 47 | this.simpleService.doSomething(); 48 | if (!context.res) { 49 | context.res = new Response(); 50 | } 51 | context.res.headers.append('passedIncontrollerGuard', 'true'); 52 | context.res.headers.append('customMetadata', customMetadata); 53 | return true; 54 | } 55 | } 56 | 57 | @Injectable() 58 | class MethodGuard implements AuthGuard { 59 | constructor(private simpleService: SimpleService) { 60 | } 61 | 62 | canActivate(context: ExecutionContext) { 63 | this.simpleService.doSomething(); 64 | const method = context.getHandler(); 65 | const customMetadata = MetadataHelper.getMetadata( 66 | 'customMetadata', 67 | method, 68 | ); 69 | if (!context.res) { 70 | context.res = new Response(); 71 | } 72 | context.res.headers.append('passedInmethodGuard', 'true'); 73 | context.res.headers.append('customMetadata', customMetadata); 74 | return true; 75 | } 76 | } 77 | 78 | @Controller('method-guard') 79 | class MethodGuardController { 80 | @SetMetadata('customMetadata', 'customValue') 81 | @UseGuard(MethodGuard) 82 | @Get('/') 83 | simpleGet() {} 84 | } 85 | 86 | @SetMetadata('customMetadata', 'customValue') 87 | @UseGuard(ControllerGuard) 88 | @Controller('controller-guard') 89 | class AuthGuardController { 90 | @Get('/') 91 | simpleGet() {} 92 | } 93 | 94 | @Module({ 95 | imports: [], 96 | controllers: [MethodGuardController], 97 | injectables: [SimpleService], 98 | }) 99 | class MethodGuardModule {} 100 | 101 | @Module({ 102 | imports: [MethodGuardModule], 103 | controllers: [AuthGuardController], 104 | }) 105 | class ControllerGuardModule {} 106 | 107 | for (const guardType of ['controller', 'method']) { 108 | Deno.test(`${guardType} guard`, async () => { 109 | const app = new DanetApplication(); 110 | await app.init(ControllerGuardModule); 111 | const listenEvent = await app.listen(0); 112 | const res = await fetch( 113 | `http://localhost:${listenEvent.port}/${guardType}-guard`, 114 | { 115 | method: 'GET', 116 | }, 117 | ); 118 | assertEquals(res.status, 200); 119 | assertEquals(res.headers.get(`passedIn${guardType}guard`), 'true'); 120 | assertEquals(res.headers.get('custommetadata'), 'customValue'); 121 | await res?.body?.cancel(); 122 | await app.close(); 123 | }); 124 | } 125 | 126 | @Controller('global-guard') 127 | class GlobalAuthController { 128 | @Get('/') 129 | simpleGet() {} 130 | } 131 | 132 | @Module({ 133 | imports: [], 134 | controllers: [GlobalAuthController], 135 | injectables: [{ useClass: GlobalGuard, token: GLOBAL_GUARD }, SimpleService], 136 | }) 137 | class GlobalAuthModule {} 138 | 139 | Deno.test('Global guard', async () => { 140 | const app = new DanetApplication(); 141 | await app.init(GlobalAuthModule); 142 | const listenEvent = await app.listen(0); 143 | const res = await fetch(`http://localhost:${listenEvent.port}/global-guard`, { 144 | method: 'GET', 145 | }); 146 | assertEquals(res.headers.get('passedinglobalguard'), 'true'); 147 | await res?.body?.cancel(); 148 | await app.close(); 149 | }); 150 | 151 | @Injectable() 152 | class ThrowingGuard implements AuthGuard { 153 | canActivate() { 154 | return false; 155 | } 156 | } 157 | 158 | @UseGuard(ThrowingGuard) 159 | @Controller('throwing-guard') 160 | class ThrowingGuardController { 161 | @Get('/') 162 | simpleGet() {} 163 | } 164 | 165 | @Module({ 166 | imports: [], 167 | injectables: [SimpleService], 168 | controllers: [ThrowingGuardController], 169 | }) 170 | class ThrowingAuthModule {} 171 | 172 | Deno.test('403 when guard is throwing', async () => { 173 | const app = new DanetApplication(); 174 | await app.init(ThrowingAuthModule); 175 | const listenEvent = await app.listen(0); 176 | const res = await fetch( 177 | `http://localhost:${listenEvent.port}/throwing-guard`, 178 | { 179 | method: 'GET', 180 | }, 181 | ); 182 | const errorStatus = res.status; 183 | assertEquals(errorStatus, 403); 184 | const json = await res.json(); 185 | assertEquals(json, { 186 | description: 'Forbidden', 187 | message: '403 - Forbidden', 188 | name: 'ForbiddenException', 189 | status: 403, 190 | }); 191 | await app.close(); 192 | }); 193 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement on 63 | [our discord](https://discord.gg/Q7ZHuDPgjA). All complaints will be reviewed 64 | and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | -------------------------------------------------------------------------------- /spec/websocket/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from '../../src/deps_test.ts'; 2 | import { DanetApplication } from '../../src/app.ts'; 3 | import { Module } from '../../src/module/decorator.ts'; 4 | import { Controller, Get } from '../../src/router/controller/decorator.ts'; 5 | import { Injectable } from '../../src/injector/injectable/decorator.ts'; 6 | import { ExecutionContext } from '../../src/router/router.ts'; 7 | import { 8 | DanetMiddleware, 9 | Middleware, 10 | NextFunction, 11 | } from '../../src/router/middleware/decorator.ts'; 12 | import { BadRequestException } from '../../src/exception/http/exceptions.ts'; 13 | import { WebSocketController } from '../../src/router/websocket/decorator.ts'; 14 | import { OnWebSocketMessage } from '../../src/router/websocket/decorator.ts'; 15 | 16 | @Injectable() 17 | class SimpleInjectable { 18 | doSomething() { 19 | return 'I did something'; 20 | } 21 | } 22 | 23 | @Injectable() 24 | class SimpleMiddleware implements DanetMiddleware { 25 | constructor(private simpleInjectable: SimpleInjectable) { 26 | } 27 | 28 | async action(ctx: ExecutionContext, next: NextFunction) { 29 | ctx.websocket?.send( 30 | JSON.stringify({ 31 | topic: 'simple-middle', 32 | data: this.simpleInjectable.doSomething(), 33 | }), 34 | ); 35 | await next(); 36 | } 37 | } 38 | 39 | const secondMiddleware = async (ctx: ExecutionContext, next: NextFunction) => { 40 | ctx.websocket?.send(JSON.stringify({ topic: 'second-middle', data: 'more' })); 41 | await next(); 42 | }; 43 | 44 | @WebSocketController('simple-controller') 45 | class SimpleController { 46 | @OnWebSocketMessage('trigger') 47 | @Middleware(SimpleMiddleware) 48 | getWithMiddleware() { 49 | } 50 | } 51 | 52 | @Middleware(SimpleMiddleware, secondMiddleware) 53 | @WebSocketController('controller-with-middleware') 54 | class ControllerWithMiddleware { 55 | @OnWebSocketMessage('trigger') 56 | getWithoutMiddleware() { 57 | } 58 | } 59 | 60 | @Module({ 61 | controllers: [SimpleController, ControllerWithMiddleware], 62 | injectables: [SimpleInjectable], 63 | }) 64 | class MyModule {} 65 | 66 | Deno.test('Middleware method decorator', async () => { 67 | return new Promise(async (resolve) => { 68 | const app = new DanetApplication(); 69 | await app.init(MyModule); 70 | const listenEvent = await app.listen(0); 71 | 72 | const websocket = new WebSocket( 73 | `ws://localhost:${listenEvent.port}/simple-controller`, 74 | ); 75 | websocket.onmessage = async (e) => { 76 | const parsedResponse = JSON.parse(e.data); 77 | assertEquals(parsedResponse.topic, 'simple-middle'); 78 | assertEquals(parsedResponse.data, 'I did something'); 79 | await app.close(); 80 | websocket.close(); 81 | resolve(); 82 | }; 83 | websocket.onopen = (e) => { 84 | websocket.send( 85 | JSON.stringify({ topic: 'trigger', data: { didIGetThisBack: true } }), 86 | ); 87 | }; 88 | }); 89 | }); 90 | 91 | Deno.test('Middleware controller decorator', async () => { 92 | return new Promise(async (resolve) => { 93 | const app = new DanetApplication(); 94 | await app.init(MyModule); 95 | const listenEvent = await app.listen(0); 96 | 97 | const websocket = new WebSocket( 98 | `ws://localhost:${listenEvent.port}/controller-with-middleware`, 99 | ); 100 | let numberOfReceivedMessage = 0; 101 | websocket.onmessage = async (e) => { 102 | const parsedResponse = JSON.parse(e.data); 103 | if (parsedResponse.topic === 'simple-middle') { 104 | assertEquals(parsedResponse.data, 'I did something'); 105 | numberOfReceivedMessage++; 106 | } else { 107 | assertEquals(parsedResponse.topic, 'second-middle'); 108 | assertEquals(parsedResponse.data, 'more'); 109 | assertEquals(numberOfReceivedMessage, 1); 110 | await app.close(); 111 | websocket.close(); 112 | resolve(); 113 | } 114 | }; 115 | websocket.onopen = (e) => { 116 | websocket.send( 117 | JSON.stringify({ topic: 'trigger', data: { didIGetThisBack: true } }), 118 | ); 119 | }; 120 | }); 121 | }); 122 | 123 | @Injectable() 124 | class FirstGlobalMiddleware implements DanetMiddleware { 125 | async action(ctx: ExecutionContext, next: NextFunction) { 126 | ctx.websocket?.send( 127 | JSON.stringify({ topic: 'first-middleware', data: 'its me' }), 128 | ); 129 | return await next(); 130 | } 131 | } 132 | 133 | @Injectable() 134 | class SecondGlobalMiddleware implements DanetMiddleware { 135 | async action(ctx: ExecutionContext, next: NextFunction) { 136 | ctx.websocket?.send( 137 | JSON.stringify({ topic: 'second-middleware', data: 'mario' }), 138 | ); 139 | return await next(); 140 | } 141 | } 142 | 143 | Deno.test('Global middlewares', async () => { 144 | return new Promise(async (resolve) => { 145 | const app = new DanetApplication(); 146 | await app.init(MyModule); 147 | app.addGlobalMiddlewares(FirstGlobalMiddleware, SecondGlobalMiddleware); 148 | const listenEvent = await app.listen(0); 149 | 150 | const websocket = new WebSocket( 151 | `ws://localhost:${listenEvent.port}/controller-with-middleware`, 152 | ); 153 | let numberOfReceivedMessage = 0; 154 | websocket.onmessage = async (e) => { 155 | const parsedResponse = JSON.parse(e.data); 156 | if (parsedResponse.topic === 'simple-middle') { 157 | assertEquals(parsedResponse.data, 'I did something'); 158 | numberOfReceivedMessage++; 159 | } else if (parsedResponse.topic === 'first-middleware') { 160 | assertEquals(parsedResponse.data, 'its me'); 161 | numberOfReceivedMessage++; 162 | } else if (parsedResponse.topic === 'second-middleware') { 163 | assertEquals(parsedResponse.data, 'mario'); 164 | numberOfReceivedMessage++; 165 | } else { 166 | assertEquals(parsedResponse.data, 'more'); 167 | assertEquals(numberOfReceivedMessage, 3); 168 | await app.close(); 169 | websocket.close(); 170 | resolve(); 171 | } 172 | }; 173 | websocket.onopen = (e) => { 174 | websocket.send( 175 | JSON.stringify({ topic: 'trigger', data: { didIGetThisBack: true } }), 176 | ); 177 | }; 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /spec/injection.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertInstanceOf, 4 | assertNotEquals, 5 | assertRejects, 6 | } from '../src/deps_test.ts'; 7 | import { DanetApplication } from '../src/app.ts'; 8 | import { GLOBAL_GUARD } from '../src/guard/constants.ts'; 9 | import { AuthGuard } from '../src/guard/interface.ts'; 10 | import { Inject } from '../src/injector/decorator.ts'; 11 | import { Injectable, SCOPE } from '../src/injector/injectable/decorator.ts'; 12 | import { Module, ModuleMetadata } from '../src/module/decorator.ts'; 13 | import { Controller, Get, Post } from '../src/router/controller/decorator.ts'; 14 | import { HttpContext } from '../src/router/router.ts'; 15 | import { injector } from '../src/injector/injector.ts'; 16 | import { Injector, TokenInjector } from '../mod.ts'; 17 | 18 | Deno.test('Injection', async (testContext) => { 19 | interface IDBService { 20 | data: string; 21 | id: string; 22 | } 23 | 24 | interface ConfigurationObject { 25 | name: string; 26 | port: number; 27 | } 28 | 29 | @Injectable() 30 | class GlobalGuard implements AuthGuard { 31 | canActivate(context: HttpContext): boolean { 32 | return true; 33 | } 34 | } 35 | 36 | @Injectable({ scope: SCOPE.GLOBAL }) 37 | class GlobalInjectable { 38 | } 39 | 40 | @Injectable({ scope: SCOPE.REQUEST }) 41 | class Child1 { 42 | public id = crypto.randomUUID(); 43 | constructor( 44 | public child: GlobalInjectable, 45 | @Inject('DB_SERVICE') public dbService: IDBService, 46 | ) { 47 | } 48 | 49 | sayHelloWorld() { 50 | return 'helloWorld'; 51 | } 52 | } 53 | 54 | @Injectable() 55 | class DatabaseService implements IDBService { 56 | public data = 'coucou'; 57 | public id = crypto.randomUUID(); 58 | constructor() { 59 | console.log('we construct'); 60 | } 61 | } 62 | 63 | @Controller('first-controller') 64 | class FirstController { 65 | public id = crypto.randomUUID(); 66 | 67 | constructor(public child1: Child1) { 68 | } 69 | 70 | @Get() 71 | getMethod() { 72 | } 73 | 74 | @Post('post') 75 | postMethod() { 76 | } 77 | } 78 | 79 | @Controller('second-controller/') 80 | class SingletonController { 81 | public id = crypto.randomUUID(); 82 | public appBoostrapCalled = false; 83 | public appCloseCalled = false; 84 | 85 | constructor( 86 | public child2: GlobalInjectable, 87 | @Inject('DB_SERVICE') public dbService: IDBService, 88 | @Inject('CONFIGURATION') public injectedPlainObject: ConfigurationObject, 89 | ) { 90 | } 91 | 92 | @Get('') 93 | getMethod() { 94 | return `${this.injectedPlainObject.name} and ${this.injectedPlainObject.port}`; 95 | } 96 | 97 | @Post('/post/') 98 | postMethod() { 99 | } 100 | } 101 | 102 | @Module({}) 103 | class SecondModule { 104 | static forRoot() { 105 | return { 106 | controllers: [SingletonController], 107 | injectables: [ 108 | GlobalInjectable, 109 | new TokenInjector(DatabaseService, 'DB_SERVICE'), 110 | { 111 | token: 'CONFIGURATION', 112 | useValue: { 113 | name: 'toto', 114 | port: '4000', 115 | }, 116 | }, 117 | ], 118 | module: SecondModule, 119 | }; 120 | } 121 | } 122 | 123 | const firstModuleOption: ModuleMetadata = { 124 | imports: [SecondModule.forRoot()], 125 | controllers: [FirstController], 126 | injectables: [ 127 | Child1, 128 | GlobalInjectable, 129 | { 130 | token: GLOBAL_GUARD, 131 | useClass: GlobalGuard, 132 | }, 133 | ], 134 | }; 135 | 136 | @Module(firstModuleOption) 137 | class FirstModule { 138 | } 139 | 140 | @Module({ 141 | controllers: [FirstController], 142 | injectables: [Child1], 143 | }) 144 | class ModuleWithMissingProvider { 145 | } 146 | 147 | await testContext.step( 148 | 'it throws if controllers dependencies are not available in context or globally', 149 | () => { 150 | const failingApp = new DanetApplication(); 151 | assertRejects(() => failingApp.init(ModuleWithMissingProvider)); 152 | }, 153 | ); 154 | 155 | const app = new DanetApplication(); 156 | await app.init(FirstModule); 157 | 158 | await testContext.step( 159 | 'it inject controllers dependencies if they are provided by current module or previously loaded module', 160 | async () => { 161 | const firstController = await app.get(FirstController)!; 162 | assertInstanceOf(firstController.child1, Child1); 163 | assertEquals(firstController.child1.sayHelloWorld(), 'helloWorld'); 164 | assertEquals(firstController.child1.dbService.data, 'coucou'); 165 | const singletonController = await app.get(SingletonController)!; 166 | assertInstanceOf(singletonController.child2, GlobalInjectable); 167 | assertInstanceOf(singletonController.dbService, DatabaseService); 168 | assertEquals( 169 | firstController.child1.dbService.id, 170 | singletonController.dbService.id, 171 | ); 172 | }, 173 | ); 174 | 175 | await testContext.step( 176 | 'controllers are singleton if none of their depency is scoped', 177 | async () => { 178 | const firstInstance = await app.get( 179 | SingletonController, 180 | )!; 181 | const secondInstance = await app.get( 182 | SingletonController, 183 | )!; 184 | assertEquals(firstInstance.id, secondInstance.id); 185 | }, 186 | ); 187 | 188 | await testContext.step( 189 | 'controllers are not singleton if one of their dependencies is request scoped', 190 | async () => { 191 | const firstInstance = await app.get(FirstController)!; 192 | const secondInstance = await app.get(FirstController)!; 193 | assertNotEquals(firstInstance.id, secondInstance.id); 194 | assertNotEquals(firstInstance.child1.id, secondInstance.child1.id); 195 | }, 196 | ); 197 | 198 | await testContext.step('it inject GLOBAL_GUARD', async () => { 199 | const globalGuard = await app.get(GLOBAL_GUARD); 200 | assertInstanceOf(globalGuard, GlobalGuard); 201 | }); 202 | 203 | await testContext.step( 204 | 'inject plain object when using useValue', 205 | async () => { 206 | const firstInstance = await app.get( 207 | SingletonController, 208 | )!; 209 | assertEquals(firstInstance.getMethod(), 'toto and 4000'); 210 | }, 211 | ); 212 | }); 213 | -------------------------------------------------------------------------------- /src/router/controller/decorator.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../../metadata/helper.ts'; 2 | import { MetadataFunction, SetMetadata } from '../../metadata/decorator.ts'; 3 | /** 4 | * Represents the HTTP methods that can be used in routing. 5 | * 6 | * @typedef {('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD')} HttpMethod 7 | * 8 | * @example 9 | * // Example usage: 10 | * const method: HttpMethod = 'GET'; 11 | */ 12 | export type HttpMethod = 13 | | 'GET' 14 | | 'POST' 15 | | 'PUT' 16 | | 'PATCH' 17 | | 'DELETE' 18 | | 'OPTIONS' 19 | | 'HEAD'; 20 | 21 | /** 22 | * Decorate a class to make it an HTTP Controller. 23 | * 24 | * @template T - The type parameter for the controller. 25 | * @param {string} [endpoint=''] - The endpoint for the controller. Defaults to an empty string. 26 | * @returns {MetadataFunction} - A function that sets the metadata for the endpoint. 27 | */ 28 | export function Controller(endpoint = ''): MetadataFunction { 29 | return SetMetadata('endpoint', endpoint); 30 | } 31 | type MappingDecoratorFunction = (endpoint?: string) => MethodDecorator; 32 | 33 | function createMappingDecorator(method?: HttpMethod): MappingDecoratorFunction { 34 | return (endpoint = ''): MethodDecorator => { 35 | return (_target, _propertyKey, descriptor) => { 36 | MetadataHelper.setMetadata('endpoint', endpoint, descriptor.value); 37 | if (method) { 38 | MetadataHelper.setMetadata('method', method, descriptor.value); 39 | } 40 | return descriptor; 41 | }; 42 | }; 43 | } 44 | 45 | /** 46 | * Define a method as a GET request handler. 47 | * 48 | * This decorator can be used to annotate controller methods to handle HTTP GET requests. 49 | * 50 | * @example 51 | * ```typescript 52 | * @Get('/path') 53 | * public getMethod(): string { 54 | * return 'This is a GET request'; 55 | * } 56 | * ``` 57 | * 58 | * @type {MappingDecoratorFunction} 59 | */ 60 | export const Get: MappingDecoratorFunction = createMappingDecorator('GET'); 61 | /** 62 | * Define a method as a POST request handler . 63 | * * @example 64 | * ```typescript 65 | * @Post('/path') 66 | * public myMethod(): string { 67 | * return 'This is a POST request'; 68 | * } 69 | * ``` 70 | */ 71 | export const Post: MappingDecoratorFunction = createMappingDecorator('POST'); 72 | /** 73 | * Define a method as a PUT request handler . 74 | * * @example 75 | * ```typescript 76 | * @Put('/path') 77 | * public myMethod(): string { 78 | * return 'This is a PUT request'; 79 | * } 80 | * ``` 81 | */ 82 | export const Put: MappingDecoratorFunction = createMappingDecorator('PUT'); 83 | /** 84 | * Define a method as a PATCH request handler . 85 | * * @example 86 | * ```typescript 87 | * @Patch('/path') 88 | * public myMethod(): string { 89 | * return 'This is a PATCH request'; 90 | * } 91 | * ``` 92 | */ 93 | export const Patch: MappingDecoratorFunction = createMappingDecorator('PATCH'); 94 | /** 95 | * Define a method as a DELETE request handler . 96 | * * @example 97 | * ```typescript 98 | * @Delete('/path') 99 | * public myMethod(): string { 100 | * return 'This is a DELETE request'; 101 | * } 102 | * ``` 103 | */ 104 | export const Delete: MappingDecoratorFunction = createMappingDecorator( 105 | 'DELETE', 106 | ); 107 | /** 108 | * Define a method as an OPTIONS request handler . 109 | * * @example 110 | * ```typescript 111 | * @Options('/path') 112 | * public myMethod(): string { 113 | * return 'This is a OPTIONS request'; 114 | * } 115 | * ``` 116 | */ 117 | export const Options: MappingDecoratorFunction = createMappingDecorator( 118 | 'OPTIONS', 119 | ); 120 | /** 121 | * Define a method as an HEAD request handler . 122 | * * @example 123 | * ```typescript 124 | * @Head('/path') 125 | * public myMethod(): string { 126 | * return 'This is a HEAD request'; 127 | * } 128 | * ``` 129 | */ 130 | export const Head: MappingDecoratorFunction = createMappingDecorator('HEAD'); 131 | /** 132 | * Define a method as a request handler for all HTTP METHOD . 133 | * * @example 134 | * ```typescript 135 | * @All('/path') 136 | * public myMethod(): string { 137 | * return 'This is potentially a GET || POST || OPTIONS || HEAD || PATCH || DELETE || PUT request'; 138 | * } 139 | * ``` 140 | */ 141 | export const All: MappingDecoratorFunction = createMappingDecorator(); 142 | /** 143 | * Define a method as a request handler that will send event with SSE. 144 | * * @example 145 | * ```typescript 146 | * @SSE('/path') 147 | * public myMethod(): eventTarget { 148 | * const eventTarget = new EventTarget(); 149 | let id = 0; 150 | const interval = setInterval(() => { 151 | if (id >= 4) { 152 | clearInterval(interval); 153 | const event = new SSEEvent({ 154 | retry: 1000, 155 | id: `${id}`, 156 | data: 'close', 157 | event: 'close', 158 | }); 159 | eventTarget.dispatchEvent(event); 160 | return; 161 | } 162 | const event = new SSEEvent({ 163 | retry: 1000, 164 | id: `${id}`, 165 | data: 'world', 166 | event: 'hello', 167 | }); 168 | eventTarget.dispatchEvent(event); 169 | id++; 170 | }, 100); 171 | return eventTarget; 172 | * } 173 | * ``` 174 | */ 175 | export const SSE: MappingDecoratorFunction = ( 176 | endpoint = '', 177 | ): MethodDecorator => { 178 | return (_target, _propertyKey, descriptor) => { 179 | MetadataHelper.setMetadata('endpoint', endpoint, descriptor.value); 180 | MetadataHelper.setMetadata('method', 'GET', descriptor.value); 181 | MetadataHelper.setMetadata('SSE', true, descriptor.value); 182 | return descriptor; 183 | }; 184 | }; 185 | 186 | 187 | /** 188 | * Define response status code for a request handler. 189 | * 190 | * @param {number} statusCode - The status code to set for the request handler. 191 | * @returns {MethodDecorator} A method decorator that sets the status code for the request handler. 192 | * 193 | * @example 194 | * ```typescript 195 | * @HttpCode(203) 196 | * @Get('/path') 197 | * public myMethod(): string { 198 | * return 'This is a GET request'; 199 | * } 200 | * ``` 201 | */ 202 | export function HttpCode(statusCode: number): MethodDecorator { 203 | return (_target, _propertyKey, descriptor) => { 204 | MetadataHelper.setMetadata('status', statusCode, descriptor.value); 205 | return descriptor; 206 | }; 207 | } -------------------------------------------------------------------------------- /src/router/websocket/router.ts: -------------------------------------------------------------------------------- 1 | import { trimSlash } from '../utils.ts'; 2 | import { Constructor } from '../../utils/constructor.ts'; 3 | import { hookName } from '../../hook/interfaces.ts'; 4 | import { 5 | FilterExecutor, 6 | GuardExecutor, 7 | HttpContext, 8 | Injector, WebSocketInstance, 9 | } from '../../mod.ts'; 10 | import { 11 | Application, 12 | getPath, 13 | HonoRequest, 14 | RegExpRouter, 15 | SmartRouter, 16 | TrieRouter, 17 | } from '../../deps.ts'; 18 | import { MetadataHelper } from '../../metadata/helper.ts'; 19 | import { 20 | ControllerConstructor, 21 | ExecutionContext, 22 | MiddlewareExecutor, 23 | } from '../mod.ts'; 24 | import { resolveMethodParam } from '../controller/params/resolver.ts'; 25 | 26 | export class WebSocketRouter { 27 | constructor( 28 | private injector: Injector, 29 | private guardExecutor: GuardExecutor = new GuardExecutor(injector), 30 | private filterExecutor: FilterExecutor = new FilterExecutor(injector), 31 | private router: Application, 32 | private middlewareExecutor: MiddlewareExecutor = new MiddlewareExecutor( 33 | injector, 34 | ), 35 | ) {} 36 | 37 | public registerController(Controller: Constructor, endpoint: string) { 38 | endpoint = trimSlash(endpoint); 39 | const path = endpoint ? ('/' + endpoint) : ''; 40 | const methods = Object.getOwnPropertyNames(Controller.prototype); 41 | const topicRouter = new SmartRouter({ 42 | routers: [new RegExpRouter(), new TrieRouter()], 43 | }); 44 | this.registerTopic(methods, Controller, topicRouter); 45 | this.router.get( 46 | path, 47 | this.handleConnectionRequest(topicRouter, Controller), 48 | ); 49 | } 50 | 51 | private handleConnectionRequest( 52 | topicRouter: SmartRouter, 53 | Controller: Constructor, 54 | ) { 55 | return async (context: HttpContext) => { 56 | const { response, socket } = Deno.upgradeWebSocket(context.req.raw); 57 | const _id = crypto.randomUUID(); 58 | (context as ExecutionContext)._id = _id; 59 | (context as ExecutionContext).getClass = () => Controller; 60 | (context as ExecutionContext).websocket = socket as WebSocketInstance; 61 | (context as ExecutionContext).websocket!.id = _id; 62 | const executionContext = context as unknown as ExecutionContext; 63 | const controllerInstance = await this.injector.get( 64 | Controller, 65 | executionContext, 66 | // deno-lint-ignore no-explicit-any 67 | ) as any; 68 | socket.onopen = this.onConnection(executionContext, Controller, socket); 69 | socket.onmessage = this.onMessage( 70 | topicRouter, 71 | Controller, 72 | controllerInstance, 73 | (context as ExecutionContext).websocket!, 74 | ); 75 | return response; 76 | }; 77 | } 78 | 79 | private onConnection( 80 | executionContext: ExecutionContext, 81 | Controller: Constructor, 82 | socket: WebSocket, 83 | ) { 84 | return async () => { 85 | try { 86 | await this.guardExecutor.executeAllRelevantGuards( 87 | executionContext, 88 | Controller, 89 | () => ({}), 90 | ); 91 | } catch (e) { 92 | socket.close(1008, 'Unauthorized'); 93 | } 94 | }; 95 | } 96 | 97 | private onMessage( 98 | topicRouter: SmartRouter, 99 | Controller: Constructor, 100 | // deno-lint-ignore no-explicit-any 101 | controllerInstance: any, 102 | socket: WebSocketInstance, 103 | ) { 104 | return async (event: MessageEvent) => { 105 | const { topic, data } = JSON.parse(event.data); 106 | const fakeRequest = new Request(`https://fakerequest.com/${topic}`, { 107 | method: 'POST', 108 | body: JSON.stringify(data), 109 | }); 110 | const [methods, foundParam] = topicRouter.match('POST', topic); 111 | const methodName = methods[0][0] as string; 112 | const messageExecutionContext = {} as ExecutionContext; 113 | 114 | // deno-lint-ignore no-explicit-any 115 | (messageExecutionContext.req as any) = new HonoRequest( 116 | fakeRequest, 117 | getPath(fakeRequest), 118 | // deno-lint-ignore no-explicit-any 119 | [methods, foundParam] as any, 120 | // deno-lint-ignore no-explicit-any 121 | ) as any; 122 | const _id = crypto.randomUUID(); 123 | messageExecutionContext._id = _id; 124 | messageExecutionContext.getClass = () => Controller; 125 | messageExecutionContext.getHandler = () => controllerInstance[methodName]; 126 | messageExecutionContext.websocketTopic = topic; 127 | messageExecutionContext.websocketMessage = data; 128 | messageExecutionContext.websocket = socket; 129 | await this.middlewareExecutor.executeAllRelevantMiddlewares( 130 | messageExecutionContext as unknown as ExecutionContext, 131 | Controller, 132 | controllerInstance[methodName], 133 | async () => { 134 | try { 135 | await this.guardExecutor.executeAllRelevantGuards( 136 | messageExecutionContext, 137 | // deno-lint-ignore no-explicit-any 138 | (() => ({})) as any, 139 | controllerInstance[methodName], 140 | ); 141 | } catch (e) { 142 | socket.close(1008, 'Unauthorized'); 143 | return; 144 | } 145 | const params = await resolveMethodParam( 146 | Controller, 147 | controllerInstance[methodName], 148 | messageExecutionContext, 149 | ); 150 | let response; 151 | try { 152 | response = await controllerInstance[methodName](...params); 153 | } catch (error) { 154 | response = await this.filterExecutor 155 | .executeControllerAndMethodFilter( 156 | messageExecutionContext, 157 | error, 158 | Controller, 159 | controllerInstance[methodName], 160 | ); 161 | } 162 | if (response) { 163 | socket.send(JSON.stringify(response)); 164 | } 165 | }, 166 | ); 167 | }; 168 | } 169 | 170 | private registerTopic( 171 | methods: string[], 172 | Controller: Constructor, 173 | topicRouter: SmartRouter, 174 | ) { 175 | for (const methodName of methods) { 176 | if ( 177 | methodName === 'constructor' || 178 | (Object.values(hookName) as string[]).includes(methodName) 179 | ) { 180 | continue; 181 | } 182 | const controllerMethod = Controller.prototype[methodName]; 183 | const topic = MetadataHelper.getMetadata( 184 | 'websocket-topic', 185 | controllerMethod, 186 | ); 187 | topicRouter.add('POST', topic, methodName); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/router/controller/params/decorators.ts: -------------------------------------------------------------------------------- 1 | import { MetadataHelper } from '../../../metadata/helper.ts'; 2 | import { ExecutionContext } from '../../router.ts'; 3 | import { validateObject } from '../../../deps.ts'; 4 | import { Constructor } from '../../../mod.ts'; 5 | import { NotValidBodyException } from '../../../exception/mod.ts'; 6 | import { argumentResolverFunctionsMetadataKey } from './constants.ts'; 7 | 8 | /** 9 | * A type representing a decorator function. 10 | * 11 | * @param target - The target constructor or any other type. 12 | * @param propertyKey - The property key of the method or undefined. 13 | * @param parameterIndex - The index of the parameter in the method's parameter list. 14 | */ 15 | export type DecoratorFunction = ( 16 | // deno-lint-ignore no-explicit-any 17 | target: Constructor | any, 18 | propertyKey: string | symbol | undefined, 19 | parameterIndex: number, 20 | ) => void; 21 | 22 | export type OptionsResolver = { 23 | // deno-lint-ignore no-explicit-any 24 | target: Constructor | any; 25 | propertyKey: string | symbol | undefined; 26 | parameterIndex: number; 27 | }; 28 | 29 | /** 30 | * A type alias for a function that resolves a value based on the provided execution context and optional resolver options. 31 | * 32 | * @param context - The execution context in which the resolver is invoked. 33 | * @param opts - Information about which class/property the decorator was attached to. 34 | * @returns The resolved value, which can be either a synchronous value or a promise that resolves to a value. 35 | */ 36 | export type Resolver = ( 37 | context: ExecutionContext, 38 | opts?: OptionsResolver, 39 | ) => unknown | Promise; 40 | 41 | 42 | /** 43 | * Creates a parameter decorator that resolves a parameter using the provided resolver function. 44 | * Optionally, an additional decorator action can be executed. 45 | * 46 | * @param parameterResolver - A function that resolves the parameter value. 47 | * @param additionalDecoratorAction - An optional additional decorator action to be executed. 48 | * @returns A decorator function that can be used to decorate a parameter. 49 | */ 50 | export function createParamDecorator( 51 | parameterResolver: Resolver, 52 | additionalDecoratorAction?: ParameterDecorator, 53 | ): () => DecoratorFunction { 54 | return () => 55 | ( 56 | // deno-lint-ignore no-explicit-any 57 | target: Constructor | any, 58 | propertyKey: string | symbol | undefined, 59 | parameterIndex: number, 60 | ) => { 61 | const argumentsResolverMap: Map = 62 | MetadataHelper.getMetadata( 63 | argumentResolverFunctionsMetadataKey, 64 | target.constructor, 65 | propertyKey, 66 | ) || new Map(); 67 | 68 | argumentsResolverMap.set( 69 | parameterIndex, 70 | (context) => 71 | parameterResolver(context, { target, propertyKey, parameterIndex }), 72 | ); 73 | 74 | MetadataHelper.setMetadata( 75 | argumentResolverFunctionsMetadataKey, 76 | argumentsResolverMap, 77 | target.constructor, 78 | propertyKey, 79 | ); 80 | 81 | if (additionalDecoratorAction) { 82 | additionalDecoratorAction(target, propertyKey, parameterIndex); 83 | } 84 | }; 85 | } 86 | 87 | /** 88 | * Get current request 89 | */ 90 | export const Req: DecoratorFunction = createParamDecorator( 91 | (context: ExecutionContext) => { 92 | return context.req; 93 | }, 94 | ); 95 | 96 | /** 97 | * Get current response 98 | */ 99 | export const Res: DecoratorFunction = createParamDecorator( 100 | (context: ExecutionContext) => { 101 | return context.res; 102 | }, 103 | ); 104 | 105 | /** 106 | * Get current request websocket instance 107 | */ 108 | export const WebSocket: DecoratorFunction = createParamDecorator( 109 | (context: ExecutionContext) => { 110 | return context.websocket; 111 | }, 112 | )(); 113 | 114 | /** 115 | * Get all headers or a specific header 116 | */ 117 | export const Header: (prop?: string) => DecoratorFunction = (prop?: string) => 118 | createParamDecorator((context: ExecutionContext) => { 119 | if (!context.req.raw.headers) { 120 | return null; 121 | } 122 | return prop ? context.req.header(prop) : context.req.raw.headers; 123 | })(); 124 | 125 | /** 126 | * Used to identify the type of the body in request parameters. 127 | * 128 | * @constant {string} BODY_TYPE_KEY 129 | */ 130 | export const BODY_TYPE_KEY = 'body-type'; 131 | 132 | /** 133 | * Get request's body or a given property 134 | */ 135 | export const Body: (prop?: string) => DecoratorFunction = (prop?: string) => 136 | createParamDecorator( 137 | async (context: ExecutionContext, opts?: OptionsResolver) => { 138 | if (!opts) { 139 | throw { 140 | status: 500, 141 | message: 'Options of Body not taken by Body decorator function', 142 | }; 143 | } 144 | 145 | let body; 146 | try { 147 | body = await context.req.json(); 148 | } catch (e) { 149 | throw e; 150 | } 151 | 152 | if (!body) { 153 | return null; 154 | } 155 | 156 | // Extract Class type of Parameter with @Body 157 | const { parameterIndex } = opts; 158 | const paramsTypesDTO: Constructor[] = MetadataHelper.getMetadata( 159 | 'design:paramtypes', 160 | opts.target, 161 | opts.propertyKey, 162 | ); 163 | 164 | const param = prop ? body[prop] : body; 165 | // Make the validation of body 166 | if (paramsTypesDTO.length > 0) { 167 | const errors = validateObject(param, paramsTypesDTO[parameterIndex]); 168 | if (errors.length > 0) { 169 | throw new NotValidBodyException(errors); 170 | } 171 | } 172 | return param; 173 | }, 174 | (target, propertyKey: string | symbol | undefined, parameterIndex) => { 175 | if (!prop) { 176 | const paramsTypesDTO: Constructor[] = MetadataHelper.getMetadata( 177 | 'design:paramtypes', 178 | target, 179 | propertyKey, 180 | ); 181 | MetadataHelper.setMetadata( 182 | BODY_TYPE_KEY, 183 | paramsTypesDTO[parameterIndex], 184 | target, 185 | propertyKey, 186 | ); 187 | } 188 | }, 189 | )(); 190 | 191 | function formatQueryValue( 192 | queryValue: string[] | undefined, 193 | value: 'first' | 'last' | 'array' | undefined, 194 | ) { 195 | if (!queryValue) { 196 | return undefined; 197 | } 198 | 199 | switch (value) { 200 | case 'first': 201 | return queryValue[0]; 202 | case 'last': 203 | return queryValue[queryValue.length - 1]; 204 | case 'array': 205 | return queryValue; 206 | default: 207 | return queryValue[0]; 208 | } 209 | } 210 | 211 | /** 212 | * Identify the type of query in the router controller parameters. 213 | * 214 | * @constant {string} QUERY_TYPE_KEY 215 | */ 216 | export const QUERY_TYPE_KEY = 'query-type'; 217 | 218 | export interface QueryOption { 219 | value?: 'first' | 'last' | 'array'; 220 | } 221 | /** 222 | * Get all query params or a given query param 223 | */ 224 | export function Query( 225 | options?: QueryOption, 226 | ): ReturnType>; 227 | export function Query( 228 | param: string, 229 | options?: QueryOption, 230 | ): ReturnType>; 231 | export function Query( 232 | pParamOrOptions?: string | QueryOption, 233 | pOptions?: QueryOption, 234 | ) { 235 | return (createParamDecorator((context: ExecutionContext) => { 236 | const param = typeof pParamOrOptions === 'string' 237 | ? pParamOrOptions 238 | : undefined; 239 | const options = typeof pParamOrOptions === 'string' 240 | ? pOptions 241 | : pParamOrOptions; 242 | 243 | if (param) { 244 | return formatQueryValue( 245 | context.req.queries(param), 246 | options?.value, 247 | ); 248 | } else { 249 | return Object.fromEntries( 250 | Array.from(Object.keys(context.req.query())) 251 | .map((key) => [ 252 | key, 253 | formatQueryValue( 254 | context.req.queries(key as string), 255 | options?.value || 'first', 256 | ), 257 | ]), 258 | ); 259 | } 260 | }, (target, propertyKey, parameterIndex) => { 261 | if ((typeof pParamOrOptions !== 'string')) { 262 | const paramsTypesDTO: Constructor[] = MetadataHelper.getMetadata( 263 | 'design:paramtypes', 264 | target, 265 | propertyKey, 266 | ); 267 | MetadataHelper.setMetadata( 268 | QUERY_TYPE_KEY, 269 | paramsTypesDTO[parameterIndex], 270 | target, 271 | propertyKey, 272 | ); 273 | } 274 | }))(); 275 | } 276 | 277 | /** 278 | * Get an url param for example /user/:userId 279 | */ 280 | export function Param(paramName: string): DecoratorFunction { 281 | return createParamDecorator((context: ExecutionContext) => { 282 | const params = context.req.param(); 283 | if (paramName) { 284 | return params?.[paramName]; 285 | } else { 286 | return params; 287 | } 288 | })(); 289 | } 290 | 291 | /** 292 | * Get Session or a given property of session 293 | */ 294 | export function Session(prop?: string): DecoratorFunction { 295 | return createParamDecorator((context: ExecutionContext) => { 296 | if (prop) { 297 | return context.get('session').get(prop); 298 | } else { 299 | return context.get('session'); 300 | } 301 | })(); 302 | } 303 | 304 | 305 | /** 306 | * Injects the current execution context into the controller method. 307 | */ 308 | export function Context(): DecoratorFunction { 309 | return createParamDecorator((context: ExecutionContext) => { 310 | return context; 311 | })(); 312 | } -------------------------------------------------------------------------------- /src/schedule/enum.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2024 Kamil Mysliwiec MIT 2 | 3 | /** 4 | * Enum representing various cron expressions for scheduling tasks. 5 | * 6 | * @enum {string} 7 | * @readonly 8 | * @property {string} EVERY_MINUTE - Runs every minute. 9 | * @property {string} EVERY_5_MINUTES - Runs every 5 minutes. 10 | * @property {string} EVERY_10_MINUTES - Runs every 10 minutes. 11 | * @property {string} EVERY_30_MINUTES - Runs every 30 minutes. 12 | * @property {string} EVERY_HOUR - Runs every hour. 13 | * @property {string} EVERY_2_HOURS - Runs every 2 hours. 14 | * @property {string} EVERY_3_HOURS - Runs every 3 hours. 15 | * @property {string} EVERY_4_HOURS - Runs every 4 hours. 16 | * @property {string} EVERY_5_HOURS - Runs every 5 hours. 17 | * @property {string} EVERY_6_HOURS - Runs every 6 hours. 18 | * @property {string} EVERY_7_HOURS - Runs every 7 hours. 19 | * @property {string} EVERY_8_HOURS - Runs every 8 hours. 20 | * @property {string} EVERY_9_HOURS - Runs every 9 hours. 21 | * @property {string} EVERY_10_HOURS - Runs every 10 hours. 22 | * @property {string} EVERY_11_HOURS - Runs every 11 hours. 23 | * @property {string} EVERY_12_HOURS - Runs every 12 hours. 24 | * @property {string} EVERY_DAY_AT_1AM - Runs every day at 1 AM. 25 | * @property {string} EVERY_DAY_AT_2AM - Runs every day at 2 AM. 26 | * @property {string} EVERY_DAY_AT_3AM - Runs every day at 3 AM. 27 | * @property {string} EVERY_DAY_AT_4AM - Runs every day at 4 AM. 28 | * @property {string} EVERY_DAY_AT_5AM - Runs every day at 5 AM. 29 | * @property {string} EVERY_DAY_AT_6AM - Runs every day at 6 AM. 30 | * @property {string} EVERY_DAY_AT_7AM - Runs every day at 7 AM. 31 | * @property {string} EVERY_DAY_AT_8AM - Runs every day at 8 AM. 32 | * @property {string} EVERY_DAY_AT_9AM - Runs every day at 9 AM. 33 | * @property {string} EVERY_DAY_AT_10AM - Runs every day at 10 AM. 34 | * @property {string} EVERY_DAY_AT_11AM - Runs every day at 11 AM. 35 | * @property {string} EVERY_DAY_AT_NOON - Runs every day at noon. 36 | * @property {string} EVERY_DAY_AT_1PM - Runs every day at 1 PM. 37 | * @property {string} EVERY_DAY_AT_2PM - Runs every day at 2 PM. 38 | * @property {string} EVERY_DAY_AT_3PM - Runs every day at 3 PM. 39 | * @property {string} EVERY_DAY_AT_4PM - Runs every day at 4 PM. 40 | * @property {string} EVERY_DAY_AT_5PM - Runs every day at 5 PM. 41 | * @property {string} EVERY_DAY_AT_6PM - Runs every day at 6 PM. 42 | * @property {string} EVERY_DAY_AT_7PM - Runs every day at 7 PM. 43 | * @property {string} EVERY_DAY_AT_8PM - Runs every day at 8 PM. 44 | * @property {string} EVERY_DAY_AT_9PM - Runs every day at 9 PM. 45 | * @property {string} EVERY_DAY_AT_10PM - Runs every day at 10 PM. 46 | * @property {string} EVERY_DAY_AT_11PM - Runs every day at 11 PM. 47 | * @property {string} EVERY_DAY_AT_MIDNIGHT - Runs every day at midnight. 48 | * @property {string} EVERY_WEEK - Runs every week. 49 | * @property {string} EVERY_WEEKDAY - Runs every weekday. 50 | * @property {string} EVERY_WEEKEND - Runs every weekend. 51 | * @property {string} EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT - Runs on the 1st day of every month at midnight. 52 | * @property {string} EVERY_1ST_DAY_OF_MONTH_AT_NOON - Runs on the 1st day of every month at noon. 53 | * @property {string} EVERY_2ND_HOUR - Runs every 2nd hour. 54 | * @property {string} EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM - Runs every 2nd hour from 1 AM through 11 PM. 55 | * @property {string} EVERY_2ND_MONTH - Runs every 2nd month. 56 | * @property {string} EVERY_QUARTER - Runs every quarter. 57 | * @property {string} EVERY_6_MONTHS - Runs every 6 months. 58 | * @property {string} EVERY_YEAR - Runs every year. 59 | * @property {string} EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM - Runs every 30 minutes between 9 AM and 5 PM. 60 | * @property {string} EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM - Runs every 30 minutes between 9 AM and 6 PM. 61 | * @property {string} EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM - Runs every 30 minutes between 10 AM and 7 PM. 62 | * @property {string} MONDAY_TO_FRIDAY_AT_1AM - Runs Monday to Friday at 1 AM. 63 | * @property {string} MONDAY_TO_FRIDAY_AT_2AM - Runs Monday to Friday at 2 AM. 64 | * @property {string} MONDAY_TO_FRIDAY_AT_3AM - Runs Monday to Friday at 3 AM. 65 | * @property {string} MONDAY_TO_FRIDAY_AT_4AM - Runs Monday to Friday at 4 AM. 66 | * @property {string} MONDAY_TO_FRIDAY_AT_5AM - Runs Monday to Friday at 5 AM. 67 | * @property {string} MONDAY_TO_FRIDAY_AT_6AM - Runs Monday to Friday at 6 AM. 68 | * @property {string} MONDAY_TO_FRIDAY_AT_7AM - Runs Monday to Friday at 7 AM. 69 | * @property {string} MONDAY_TO_FRIDAY_AT_8AM - Runs Monday to Friday at 8 AM. 70 | * @property {string} MONDAY_TO_FRIDAY_AT_9AM - Runs Monday to Friday at 9 AM. 71 | * @property {string} MONDAY_TO_FRIDAY_AT_09_30AM - Runs Monday to Friday at 9:30 AM. 72 | * @property {string} MONDAY_TO_FRIDAY_AT_10AM - Runs Monday to Friday at 10 AM. 73 | * @property {string} MONDAY_TO_FRIDAY_AT_11AM - Runs Monday to Friday at 11 AM. 74 | * @property {string} MONDAY_TO_FRIDAY_AT_11_30AM - Runs Monday to Friday at 11:30 AM. 75 | * @property {string} MONDAY_TO_FRIDAY_AT_12PM - Runs Monday to Friday at 12 PM. 76 | * @property {string} MONDAY_TO_FRIDAY_AT_1PM - Runs Monday to Friday at 1 PM. 77 | * @property {string} MONDAY_TO_FRIDAY_AT_2PM - Runs Monday to Friday at 2 PM. 78 | * @property {string} MONDAY_TO_FRIDAY_AT_3PM - Runs Monday to Friday at 3 PM. 79 | * @property {string} MONDAY_TO_FRIDAY_AT_4PM - Runs Monday to Friday at 4 PM. 80 | * @property {string} MONDAY_TO_FRIDAY_AT_5PM - Runs Monday to Friday at 5 PM. 81 | * @property {string} MONDAY_TO_FRIDAY_AT_6PM - Runs Monday to Friday at 6 PM. 82 | * @property {string} MONDAY_TO_FRIDAY_AT_7PM - Runs Monday to Friday at 7 PM. 83 | * @property {string} MONDAY_TO_FRIDAY_AT_8PM - Runs Monday to Friday at 8 PM. 84 | * @property {string} MONDAY_TO_FRIDAY_AT_9PM - Runs Monday to Friday at 9 PM. 85 | * @property {string} MONDAY_TO_FRIDAY_AT_10PM - Runs Monday to Friday at 10 PM. 86 | * @property {string} MONDAY_TO_FRIDAY_AT_11PM - Runs Monday to Friday at 11 PM. 87 | */ 88 | export enum CronExpression { 89 | EVERY_MINUTE = '*/1 * * * *', 90 | EVERY_5_MINUTES = '*/5 * * * *', 91 | EVERY_10_MINUTES = '*/10 * * * *', 92 | EVERY_30_MINUTES = '*/30 * * * *', 93 | EVERY_HOUR = '0 0-23/1 * * *', 94 | EVERY_2_HOURS = '0 0-23/2 * * *', 95 | EVERY_3_HOURS = '0 0-23/3 * * *', 96 | EVERY_4_HOURS = '0 0-23/4 * * *', 97 | EVERY_5_HOURS = '0 0-23/5 * * *', 98 | EVERY_6_HOURS = '0 0-23/6 * * *', 99 | EVERY_7_HOURS = '0 0-23/7 * * *', 100 | EVERY_8_HOURS = '0 0-23/8 * * *', 101 | EVERY_9_HOURS = '0 0-23/9 * * *', 102 | EVERY_10_HOURS = '0 0-23/10 * * *', 103 | EVERY_11_HOURS = '0 0-23/11 * * *', 104 | EVERY_12_HOURS = '0 0-23/12 * * *', 105 | EVERY_DAY_AT_1AM = '0 01 * * *', 106 | EVERY_DAY_AT_2AM = '0 02 * * *', 107 | EVERY_DAY_AT_3AM = '0 03 * * *', 108 | EVERY_DAY_AT_4AM = '0 04 * * *', 109 | EVERY_DAY_AT_5AM = '0 05 * * *', 110 | EVERY_DAY_AT_6AM = '0 06 * * *', 111 | EVERY_DAY_AT_7AM = '0 07 * * *', 112 | EVERY_DAY_AT_8AM = '0 08 * * *', 113 | EVERY_DAY_AT_9AM = '0 09 * * *', 114 | EVERY_DAY_AT_10AM = '0 10 * * *', 115 | EVERY_DAY_AT_11AM = '0 11 * * *', 116 | EVERY_DAY_AT_NOON = '0 12 * * *', 117 | EVERY_DAY_AT_1PM = '0 13 * * *', 118 | EVERY_DAY_AT_2PM = '0 14 * * *', 119 | EVERY_DAY_AT_3PM = '0 15 * * *', 120 | EVERY_DAY_AT_4PM = '0 16 * * *', 121 | EVERY_DAY_AT_5PM = '0 17 * * *', 122 | EVERY_DAY_AT_6PM = '0 18 * * *', 123 | EVERY_DAY_AT_7PM = '0 19 * * *', 124 | EVERY_DAY_AT_8PM = '0 20 * * *', 125 | EVERY_DAY_AT_9PM = '0 21 * * *', 126 | EVERY_DAY_AT_10PM = '0 22 * * *', 127 | EVERY_DAY_AT_11PM = '0 23 * * *', 128 | EVERY_DAY_AT_MIDNIGHT = '0 0 * * *', 129 | EVERY_WEEK = '0 0 * * 0', 130 | EVERY_WEEKDAY = '0 0 * * 1-5', 131 | EVERY_WEEKEND = '0 0 * * 6,0', 132 | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT = '0 0 1 * *', 133 | EVERY_1ST_DAY_OF_MONTH_AT_NOON = '0 12 1 * *', 134 | EVERY_2ND_HOUR = '0 */2 * * *', 135 | EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM = '0 1-23/2 * * *', 136 | EVERY_2ND_MONTH = '0 0 1 */2 *', 137 | EVERY_QUARTER = '0 0 1 */3 *', 138 | EVERY_6_MONTHS = '0 0 1 */6 *', 139 | EVERY_YEAR = '0 0 1 0 *', 140 | EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM = '0 */30 9-17 * * *', 141 | EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM = '0 */30 9-18 * * *', 142 | EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM = '0 */30 10-19 * * *', 143 | MONDAY_TO_FRIDAY_AT_1AM = '0 0 01 * * 1-5', 144 | MONDAY_TO_FRIDAY_AT_2AM = '0 0 02 * * 1-5', 145 | MONDAY_TO_FRIDAY_AT_3AM = '0 0 03 * * 1-5', 146 | MONDAY_TO_FRIDAY_AT_4AM = '0 0 04 * * 1-5', 147 | MONDAY_TO_FRIDAY_AT_5AM = '0 0 05 * * 1-5', 148 | MONDAY_TO_FRIDAY_AT_6AM = '0 0 06 * * 1-5', 149 | MONDAY_TO_FRIDAY_AT_7AM = '0 0 07 * * 1-5', 150 | MONDAY_TO_FRIDAY_AT_8AM = '0 0 08 * * 1-5', 151 | MONDAY_TO_FRIDAY_AT_9AM = '0 0 09 * * 1-5', 152 | MONDAY_TO_FRIDAY_AT_09_30AM = '0 30 09 * * 1-5', 153 | MONDAY_TO_FRIDAY_AT_10AM = '0 0 10 * * 1-5', 154 | MONDAY_TO_FRIDAY_AT_11AM = '0 0 11 * * 1-5', 155 | MONDAY_TO_FRIDAY_AT_11_30AM = '0 30 11 * * 1-5', 156 | MONDAY_TO_FRIDAY_AT_12PM = '0 0 12 * * 1-5', 157 | MONDAY_TO_FRIDAY_AT_1PM = '0 0 13 * * 1-5', 158 | MONDAY_TO_FRIDAY_AT_2PM = '0 0 14 * * 1-5', 159 | MONDAY_TO_FRIDAY_AT_3PM = '0 0 15 * * 1-5', 160 | MONDAY_TO_FRIDAY_AT_4PM = '0 0 16 * * 1-5', 161 | MONDAY_TO_FRIDAY_AT_5PM = '0 0 17 * * 1-5', 162 | MONDAY_TO_FRIDAY_AT_6PM = '0 0 18 * * 1-5', 163 | MONDAY_TO_FRIDAY_AT_7PM = '0 0 19 * * 1-5', 164 | MONDAY_TO_FRIDAY_AT_8PM = '0 0 20 * * 1-5', 165 | MONDAY_TO_FRIDAY_AT_9PM = '0 0 21 * * 1-5', 166 | MONDAY_TO_FRIDAY_AT_10PM = '0 0 22 * * 1-5', 167 | MONDAY_TO_FRIDAY_AT_11PM = '0 0 23 * * 1-5', 168 | } 169 | 170 | /** 171 | * Various time intervals in milliseconds. 172 | * 173 | * @enum {number} 174 | * @readonly 175 | * @property {number} MILISECOND - Represents one millisecond. 176 | * @property {number} SECOND - Represents one second (1000 milliseconds). 177 | * @property {number} MINUTE - Represents one minute (60,000 milliseconds). 178 | * @property {number} HOUR - Represents one hour (3,600,000 milliseconds). 179 | * @property {number} DAY - Represents one day (86,400,000 milliseconds). 180 | */ 181 | export enum IntervalExpression { 182 | MILISECOND = 1, 183 | SECOND = 1000, 184 | MINUTE = 1000 * 60, 185 | HOUR = 1000 * 60 * 60, 186 | DAY = 1000 * 60 * 60 * 24, 187 | } 188 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * DanetApplication class where everything begins. 4 | * @example 5 | * ```typescript 6 | * import { 7 | * Controller, 8 | * DanetApplication, 9 | * Get, 10 | * Module, 11 | * Query, 12 | * } from '../src/mod.ts'; 13 | * 14 | * @Controller('') 15 | * class FirstController { 16 | * constructor() { 17 | * } 18 | * 19 | * @Get('hello-world/:name') 20 | * getHelloWorld( 21 | * @Param('name') name: string, 22 | * ) { 23 | * return `Hello World ${name}`; 24 | * } 25 | * } 26 | * 27 | * @Module({ 28 | * controllers: [FirstController] 29 | * }) 30 | * class FirstModule {} 31 | * 32 | * const app = new DanetApplication(); 33 | * await app.init(FirstModule); 34 | * 35 | * let port = Number(Deno.env.get('PORT')); 36 | * if (isNaN(port)) { 37 | * port = 3000; 38 | * } 39 | * app.listen(port); 40 | * ``` 41 | */ 42 | 43 | import { Application, MiddlewareHandler } from './deps.ts'; 44 | import { FilterExecutor } from './exception/filter/executor.ts'; 45 | import { GuardExecutor } from './guard/executor.ts'; 46 | import { HookExecutor } from './hook/executor.ts'; 47 | import { hookName } from './hook/interfaces.ts'; 48 | 49 | import { injector } from './injector/injector.ts'; 50 | import { Logger } from './logger.ts'; 51 | import { MetadataHelper } from './metadata/helper.ts'; 52 | import { ModuleMetadata, moduleMetadataKey } from './module/decorator.ts'; 53 | import { DanetHTTPRouter } from './router/router.ts'; 54 | import { WebSocketRouter } from './router/websocket/router.ts'; 55 | import { Constructor } from './utils/constructor.ts'; 56 | import { PossibleMiddlewareType } from './router/middleware/decorator.ts'; 57 | import { globalMiddlewareContainer } from './router/middleware/global-container.ts'; 58 | import { ModuleConstructor } from './module/constructor.ts'; 59 | import { serveStatic } from './utils/serve-static.ts'; 60 | import { cors } from './deps.ts'; 61 | import { DynamicModule, ExceptionFilter } from './mod.ts'; 62 | import { Renderer } from './renderer/interface.ts'; 63 | import { globalExceptionFilterContainer } from './exception/filter/global-container.ts'; 64 | 65 | type CORSOptions = { 66 | origin: string | string[] | ((origin: string) => string | undefined | null); 67 | allowMethods?: string[]; 68 | allowHeaders?: string[]; 69 | maxAge?: number; 70 | credentials?: boolean; 71 | exposeHeaders?: string[]; 72 | }; 73 | 74 | /** 75 | * DanetApplication is the main application class for initializing and managing the lifecycle of the application. 76 | * It provides methods for bootstrapping modules, registering controllers, and configuring middleware. 77 | * It also provides methods for starting and stopping the application. 78 | */ 79 | export class DanetApplication { 80 | private app: Application = new Application({ strict: false }); 81 | private internalHttpServer?: Deno.HttpServer; 82 | private injector = injector; 83 | private hookExecutor = new HookExecutor(this.injector); 84 | private renderer: Renderer | undefined = undefined; 85 | private guardExecutor = new GuardExecutor(this.injector); 86 | private filterExecutor = new FilterExecutor(this.injector); 87 | public httpRouter: DanetHTTPRouter = new DanetHTTPRouter( 88 | this.injector, 89 | this.guardExecutor, 90 | this.filterExecutor, 91 | this.renderer, 92 | this.app, 93 | ); 94 | public websocketRouter: WebSocketRouter = new WebSocketRouter( 95 | this.injector, 96 | this.guardExecutor, 97 | this.filterExecutor, 98 | this.app, 99 | ); 100 | private controller: AbortController = new AbortController(); 101 | private logger: Logger = new Logger('DanetApplication'); 102 | public entryModule!: ModuleConstructor; 103 | 104 | /** 105 | * Retrieves an instance of the specified type from the injector. 106 | * 107 | * @template T - The type of the instance to retrieve. 108 | * @param {Constructor | string} Type - The constructor of the type or a string identifier. 109 | * @returns {T} - The instance of the specified type. 110 | */ 111 | get(Type: Constructor | string): T { 112 | return this.injector.get(Type); 113 | } 114 | 115 | /** 116 | * Bootstraps the application by initializing the provided module and its dependencies. 117 | * 118 | * @param NormalOrDynamicModule - The module to bootstrap, which can be either a normal constructor or a dynamic module. 119 | * 120 | * This method performs the following steps: 121 | * 1. Determines if the provided module is a normal constructor or a dynamic module. 122 | * 2. Initializes the module and retrieves its metadata (controllers, imports, injectables). 123 | * 3. Recursively bootstraps all imported modules. 124 | * 4. Bootstraps the module using the injector. 125 | * 5. Registers controllers with either the HTTP router or WebSocket router based on their metadata. 126 | * 127 | * @template Constructor - A class constructor type. 128 | * @template DynamicModule - A type representing a dynamic module with metadata. 129 | * @template ModuleMetadata - A type representing the metadata of a module. 130 | */ 131 | async bootstrap(NormalOrDynamicModule: Constructor | DynamicModule) { 132 | // deno-lint-ignore no-explicit-any 133 | const possibleModuleInstance = NormalOrDynamicModule as any; 134 | let instance: ModuleMetadata; 135 | 136 | if ( 137 | !possibleModuleInstance.module 138 | ) { 139 | instance = new (NormalOrDynamicModule as Constructor)() as DynamicModule; 140 | const metadata: ModuleMetadata = MetadataHelper.getMetadata< 141 | ModuleMetadata 142 | >( 143 | moduleMetadataKey, 144 | NormalOrDynamicModule, 145 | ); 146 | instance.controllers = metadata.controllers; 147 | instance.imports = metadata.imports; 148 | instance.injectables = metadata.injectables; 149 | } else { 150 | instance = new ((NormalOrDynamicModule as DynamicModule) 151 | .module)() as ModuleMetadata; 152 | instance.controllers = 153 | (NormalOrDynamicModule as DynamicModule).controllers; 154 | instance.imports = (NormalOrDynamicModule as DynamicModule).imports; 155 | instance.injectables = 156 | (NormalOrDynamicModule as DynamicModule).injectables; 157 | } 158 | 159 | for (const module in instance?.imports) { 160 | // deno-lint-ignore no-explicit-any 161 | await this.bootstrap(instance.imports[module as any]); 162 | } 163 | 164 | await this.injector.bootstrapModule(instance); 165 | 166 | if (instance.controllers) { 167 | instance.controllers.forEach((Controller) => { 168 | const httpEndpoint = MetadataHelper.getMetadata( 169 | 'endpoint', 170 | Controller, 171 | ); 172 | const webSocketEndpoint = MetadataHelper.getMetadata( 173 | 'websocket-endpoint', 174 | Controller, 175 | ); 176 | if (webSocketEndpoint) { 177 | this.websocketRouter.registerController( 178 | Controller, 179 | webSocketEndpoint, 180 | ); 181 | } else { 182 | this.httpRouter.registerController(Controller, httpEndpoint); 183 | } 184 | }); 185 | } 186 | } 187 | 188 | /** 189 | * Initializes the application with the provided module. 190 | * 191 | * @param Module - The constructor of the module to initialize. 192 | * @returns A promise that resolves when the initialization process is complete. 193 | */ 194 | async init(Module: Constructor) { 195 | this.entryModule = Module; 196 | await this.bootstrap(Module); 197 | await this.hookExecutor.executeHookForEveryInjectable( 198 | hookName.APP_BOOTSTRAP, 199 | ); 200 | } 201 | 202 | /** 203 | * Closes the application by executing the necessary hooks and shutting down the internal HTTP server. 204 | * 205 | * @async 206 | * @returns {Promise} A promise that resolves when the application has been closed. 207 | */ 208 | async close() { 209 | await this.hookExecutor.executeHookForEveryInjectable(hookName.APP_CLOSE); 210 | await this.internalHttpServer?.shutdown(); 211 | this.logger.log('Shutting down'); 212 | } 213 | 214 | /** 215 | * Starts the HTTP server and begins listening on the specified port. 216 | * 217 | * @param {number} [port=3000] - The port number on which the server will listen. 218 | * @returns {Promise<{ port: number }>} A promise that resolves with an object containing the port number. 219 | * 220 | * @remarks 221 | * This method initializes an `AbortController` to manage the server's lifecycle and uses Deno's `serve` function to start the server. 222 | * The server will log a message indicating the port it is listening on. 223 | * 224 | * @example 225 | * ```typescript 226 | * const app = new DanetApplication(); 227 | * await app.init(FirstModule); 228 | * const { port } = app.listen(3000); 229 | * ``` 230 | */ 231 | listen(port = 3000): Promise<{ port: number }> { 232 | this.controller = new AbortController(); 233 | const { signal } = this.controller; 234 | const listen = new Promise<{ port: number }>((resolve) => { 235 | this.internalHttpServer = Deno.serve({ 236 | signal, 237 | port, 238 | onListen: (listen) => { 239 | this.logger.log(`Listening on ${listen.port}`); 240 | resolve({ ...listen }); 241 | }, 242 | }, this.app.fetch); 243 | }); 244 | return listen; 245 | } 246 | 247 | /** 248 | * Get hono application instance. 249 | * 250 | * @returns {Application} The hono instance. 251 | */ 252 | get router(): Application { 253 | return this.app; 254 | } 255 | 256 | /** 257 | * Set renderer 258 | */ 259 | setRenderer(renderer: Renderer) { 260 | this.renderer = renderer; 261 | this.httpRouter.setRenderer(this.renderer); 262 | } 263 | 264 | /** 265 | * Sets the directory for the view engine. 266 | * 267 | * @param path - The path to the directory to be set as the root for the view engine. 268 | */ 269 | setViewEngineDir(path: string) { 270 | if (this.renderer) { 271 | this.renderer.setRootDir(path); 272 | } 273 | } 274 | 275 | /** 276 | * Configures the application to serve static assets from the specified path. 277 | * 278 | * @param path - The file system path from which to serve static assets. 279 | */ 280 | useStaticAssets(path: string) { 281 | this.app.use('*', (context, next: () => Promise) => { 282 | const root = path; 283 | return (serveStatic({ root })(context, next)); 284 | }); 285 | } 286 | 287 | /** 288 | * Adds one or more global middlewares to the global middleware container. 289 | * 290 | * @param {...PossibleMiddlewareType[]} middlewares - The middlewares to be added to the global container. 291 | */ 292 | addGlobalMiddlewares(...middlewares: PossibleMiddlewareType[]) { 293 | globalMiddlewareContainer.push(...middlewares); 294 | } 295 | 296 | /** 297 | * Enables Cross-Origin Resource Sharing (CORS) for the application. 298 | * 299 | * @param {CORSOptions} [options] - Optional configuration for CORS. 300 | */ 301 | enableCors(options?: CORSOptions) { 302 | this.app.use('*', cors(options)); 303 | } 304 | 305 | /** 306 | * Registers a hono middleware handler to be used for all routes. 307 | * 308 | * @param middleware - The middleware handler to be used. 309 | */ 310 | use(middleware: MiddlewareHandler) { 311 | this.app.use('*', middleware); 312 | } 313 | 314 | 315 | /** 316 | * Register a base path for the application. 317 | * 318 | * @param basePath - The base path to be registered. 319 | */ 320 | registerBasePath(basePath: string) { 321 | this.httpRouter.setPrefix(basePath); 322 | } 323 | 324 | /** 325 | * Add a global exception filter to the global exception filter container. 326 | * 327 | * @param errorFilter - The exception filter to be added. 328 | */ 329 | useGlobalExceptionFilter(errorFilter: ExceptionFilter) { 330 | globalExceptionFilterContainer.push(errorFilter); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/router/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | Context, 4 | type HandlerInterface, 5 | SSEStreamingApi, 6 | streamSSE, 7 | } from '../deps.ts'; 8 | 9 | import { FilterExecutor } from '../exception/filter/executor.ts'; 10 | import { HTTP_STATUS } from '../exception/http/enum.ts'; 11 | import { GuardExecutor } from '../guard/executor.ts'; 12 | import { hookName } from '../hook/interfaces.ts'; 13 | import { Injector } from '../injector/injector.ts'; 14 | import { Logger } from '../logger.ts'; 15 | import { MetadataHelper } from '../metadata/helper.ts'; 16 | import { rendererViewFile } from '../renderer/decorator.ts'; 17 | import { Renderer } from '../renderer/interface.ts'; 18 | import { Constructor } from '../utils/constructor.ts'; 19 | import { ControllerConstructor } from './controller/constructor.ts'; 20 | import { trimSlash } from './utils.ts'; 21 | import { MiddlewareExecutor } from './middleware/executor.ts'; 22 | import { NextFunction } from './middleware/decorator.ts'; 23 | import { resolveMethodParam } from './controller/params/resolver.ts'; 24 | import { SSEEvent } from '../sse/event.ts'; 25 | 26 | /** 27 | * Type to define a callback function. 28 | */ 29 | // deno-lint-ignore no-explicit-any 30 | export type Callback = (...args: any[]) => unknown; 31 | 32 | /** 33 | * Type Alias for Hono's Context 34 | */ 35 | export type HttpContext = Context; 36 | 37 | /** Type for WebSocket instance */ 38 | export type WebSocketInstance = WebSocket & { id: string }; 39 | 40 | /** 41 | * Represents Danet's execution context for an HTTP request, extending Hono's HttpContext. 42 | * 43 | * @typedef {Object} ExecutionContext 44 | * @property {string} _id - Unique identifier for the execution context. 45 | * @property {Function} getHandler - Function to retrieve the handler for the current context. 46 | * @property {Constructor} getClass - Function to retrieve the class constructor for the current context. 47 | * @property {WebSocketInstance} [websocket] - Optional WebSocket instance associated with the context. 48 | * @property {any} [websocketMessage] - Optional message received via WebSocket. 49 | * @property {string} [websocketTopic] - Optional topic associated with the WebSocket message. 50 | */ 51 | export type ExecutionContext = HttpContext & { 52 | _id: string; 53 | // deno-lint-ignore ban-types 54 | getHandler: () => Function; 55 | getClass: () => Constructor; 56 | websocket?: WebSocketInstance; 57 | // deno-lint-ignore no-explicit-any 58 | websocketMessage?: any; 59 | websocketTopic?: string; 60 | }; 61 | 62 | /** 63 | * The `DanetHTTPRouter` class is responsible for managing HTTP routes and their associated handlers. 64 | * It provides methods to register controllers, set route prefixes, and handle middleware, guards, filters, and responses. 65 | * 66 | * @class DanetHTTPRouter 67 | */ 68 | export class DanetHTTPRouter { 69 | private logger: Logger = new Logger('Router'); 70 | private methodsMap: Map; 71 | public prefix?: string; 72 | private middlewareExecutor: MiddlewareExecutor; 73 | constructor( 74 | private injector: Injector, 75 | private guardExecutor: GuardExecutor = new GuardExecutor(injector), 76 | private filterExecutor: FilterExecutor = new FilterExecutor(injector), 77 | private viewRenderer: Renderer | undefined, 78 | private router: Application, 79 | ) { 80 | this.methodsMap = new Map([ 81 | ['DELETE', this.router.delete], 82 | ['GET', this.router.get], 83 | ['PATCH', this.router.patch], 84 | ['POST', this.router.post], 85 | ['PUT', this.router.put], 86 | ['OPTIONS', this.router.options], 87 | ['HEAD', this.router.get], 88 | ['ALL', this.router.all], 89 | ]); 90 | this.middlewareExecutor = new MiddlewareExecutor( 91 | injector, 92 | ); 93 | } 94 | 95 | setRenderer(renderer: Renderer) { 96 | this.viewRenderer = renderer; 97 | } 98 | 99 | private createRoute( 100 | handlerName: string | hookName, 101 | Controller: Constructor, 102 | basePath: string, 103 | ) { 104 | if ( 105 | handlerName === 'constructor' || 106 | (Object.values(hookName) as string[]).includes(handlerName) 107 | ) { 108 | return; 109 | } 110 | const controllerMethod = Controller.prototype[handlerName]; 111 | let endpoint = MetadataHelper.getMetadata( 112 | 'endpoint', 113 | controllerMethod, 114 | ); 115 | 116 | basePath = trimSlash(basePath); 117 | endpoint = trimSlash(endpoint); 118 | const path = (basePath ? ('/' + basePath) : '') + 119 | (endpoint ? '/' + endpoint : ''); 120 | 121 | const httpMethod = MetadataHelper.getMetadata( 122 | 'method', 123 | controllerMethod, 124 | ); 125 | const routerFn = this.methodsMap.get(httpMethod || 'ALL'); 126 | if (!routerFn) { 127 | throw new Error( 128 | `The method "${httpMethod}" can not be handled by "${basePath}" of controller "${Controller}".`, 129 | ); 130 | } 131 | const routePath = `${this.prefix ? this.prefix : ''}${ 132 | path ? path : '/' 133 | }`; 134 | this.logger.log( 135 | `Registering [${httpMethod}] ${routePath}`, 136 | ); 137 | routerFn( 138 | routePath, 139 | async (context: HttpContext, next: NextFunction) => { 140 | const _id = crypto.randomUUID(); 141 | (context as ExecutionContext)._id = _id; 142 | (context as ExecutionContext).getClass = () => Controller; 143 | (context as ExecutionContext).getHandler = () => controllerMethod; 144 | context.res = new Response(); 145 | context.set('_id', _id); 146 | try { 147 | await this.middlewareExecutor.executeAllRelevantMiddlewares( 148 | context as unknown as ExecutionContext, 149 | Controller, 150 | controllerMethod, 151 | next, 152 | ); 153 | } catch (error) { 154 | return this.handleError( 155 | context as ExecutionContext, 156 | error, 157 | Controller, 158 | controllerMethod, 159 | ); 160 | } 161 | }, 162 | this.handleRoute(Controller, controllerMethod), 163 | ); 164 | } 165 | 166 | /** 167 | * Sets the prefix for the router, ensuring that it does not end with a trailing slash. 168 | * 169 | * @param prefix - The prefix string to set for the router. 170 | */ 171 | setPrefix(prefix: string) { 172 | if (prefix.endsWith('/')) { 173 | prefix = prefix.slice(0, -1); 174 | } 175 | this.prefix = prefix; 176 | } 177 | 178 | /** 179 | * Registers a controller and its methods as routes. 180 | * 181 | * @param Controller - The controller class to register. 182 | * @param basePath - The base path for the controller's routes. 183 | */ 184 | public registerController(Controller: Constructor, basePath: string) { 185 | const methods = Object.getOwnPropertyNames(Controller.prototype); 186 | this.logger.log( 187 | `Registering ${Controller.name} ${basePath ? basePath : '/'}`, 188 | ); 189 | methods.forEach((methodName) => { 190 | this.createRoute(methodName, Controller, basePath); 191 | }); 192 | } 193 | 194 | private handleRoute( 195 | Controller: ControllerConstructor, 196 | ControllerMethod: Callback, 197 | ): (context: HttpContext) => Promise { 198 | return async (context: HttpContext) => { 199 | (context as ExecutionContext)._id = context.get('_id'); 200 | (context as ExecutionContext).getClass = () => Controller; 201 | (context as ExecutionContext).getHandler = () => ControllerMethod; 202 | if (!context.res) { 203 | context.res = new Response(); 204 | } 205 | try { 206 | const executionContext = context as unknown as ExecutionContext; 207 | await this.guardExecutor.executeAllRelevantGuards( 208 | executionContext, 209 | Controller, 210 | ControllerMethod, 211 | ); 212 | const params = await resolveMethodParam( 213 | Controller, 214 | ControllerMethod, 215 | executionContext, 216 | ); 217 | const controllerInstance = await this.injector.get( 218 | Controller, 219 | executionContext, 220 | // deno-lint-ignore no-explicit-any 221 | ) as any; 222 | const response: 223 | | Record 224 | | string | Response = await controllerInstance[ControllerMethod.name]( 225 | ...params, 226 | ); 227 | const isSSE = MetadataHelper.getMetadata('SSE', ControllerMethod); 228 | if (isSSE) { 229 | context.res = this.handleSSE( 230 | executionContext, 231 | response as unknown as EventTarget, 232 | ); 233 | return context.res; 234 | } 235 | if (response instanceof Response) { 236 | context.res = response; 237 | return context.res; 238 | } 239 | return await this.sendResponse( 240 | response, 241 | ControllerMethod, 242 | executionContext, 243 | ); 244 | } catch (error) { 245 | return this.handleError( 246 | context as ExecutionContext, 247 | error, 248 | Controller, 249 | ControllerMethod, 250 | ); 251 | } 252 | }; 253 | } 254 | 255 | private handleSSE(executionContext: ExecutionContext, response: EventTarget) { 256 | return streamSSE(executionContext, async (stream: SSEStreamingApi) => { 257 | let canContinue = true; 258 | response.addEventListener( 259 | 'message', 260 | async (event) => { 261 | const { detail: payload } = event as SSEEvent; 262 | const dataAsString = typeof payload.data === 'object' ? JSON.stringify(payload.data) : payload.data; 263 | await stream.writeSSE({ 264 | data: dataAsString, 265 | event: payload.event, 266 | id: payload.id, 267 | retry: payload.retry, 268 | }); 269 | if (payload.event === 'close') { 270 | canContinue = false; 271 | } 272 | }, 273 | ); 274 | while (canContinue) { 275 | await stream.sleep(1); 276 | } 277 | await stream.close(); 278 | }); 279 | } 280 | 281 | private async handleError( 282 | executionContext: ExecutionContext, 283 | // deno-lint-ignore no-explicit-any 284 | error: any, 285 | Controller: ControllerConstructor, 286 | // deno-lint-ignore no-explicit-any 287 | ControllerMethod: (...args: any[]) => unknown, 288 | ) { 289 | const filterResponse = await this.filterExecutor 290 | .executeControllerAndMethodFilter( 291 | executionContext, 292 | error, 293 | Controller, 294 | ControllerMethod, 295 | ); 296 | if (filterResponse) { 297 | executionContext.res = filterResponse as Response; 298 | return executionContext.res; 299 | } 300 | const status = error.status || HTTP_STATUS.INTERNAL_SERVER_ERROR; 301 | const message = error.message || 'Internal server error!'; 302 | this.injector.cleanRequestInjectables(executionContext._id); 303 | executionContext.res = executionContext.json({ 304 | ...error, 305 | status, 306 | message, 307 | }, status); 308 | return executionContext.res; 309 | } 310 | 311 | private async sendResponse( 312 | response: string | Record, 313 | ControllerMethod: Callback, 314 | context: HttpContext, 315 | ) { 316 | const status = MetadataHelper.getMetadata('status', ControllerMethod) || 200; 317 | if (response) { 318 | const fileName = MetadataHelper.getMetadata( 319 | rendererViewFile, 320 | ControllerMethod, 321 | ); 322 | if (fileName && this.viewRenderer) { 323 | context.res = await context.html( 324 | await this.viewRenderer.render( 325 | fileName, 326 | response, 327 | ), 328 | { 329 | headers: context.res.headers, 330 | }, 331 | ); 332 | } else { 333 | if (typeof response !== 'string') { 334 | context.res = context.json(response, { 335 | headers: context.res.headers, 336 | status, 337 | }); 338 | } else { 339 | context.res = context.text(response, { 340 | headers: context.res.headers, 341 | status, 342 | }); 343 | } 344 | } 345 | } 346 | context.res = context.body(context.res.body, { 347 | headers: context.res.headers, 348 | status, 349 | }); 350 | return context.res; 351 | } 352 | } 353 | --------------------------------------------------------------------------------