Skip to content

Commit

Permalink
Merge pull request #15 from medishen/dev/v2
Browse files Browse the repository at this point in the history
Fix event system and enhance decorators with unit tests
  • Loading branch information
0xii00 authored Jan 17, 2025
2 parents 09579a1 + ea971ca commit d8cfaec
Show file tree
Hide file tree
Showing 29 changed files with 1,393 additions and 123 deletions.
7 changes: 0 additions & 7 deletions examples/.confmodule

This file was deleted.

7 changes: 0 additions & 7 deletions examples/app.ts

This file was deleted.

10 changes: 0 additions & 10 deletions examples/router/index.ts

This file was deleted.

10 changes: 0 additions & 10 deletions examples/router/test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion lib/common/enums/router.enum.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export enum RouterMetadataKeys {
CONTROLLER_PREFIX = 'router:controller:prefix',
ROUTES = 'router:routes',
MULTI_LANG = 'router:multiLang',
MULTI_LANGUAGE = 'router:multiLanguage',
TRANSFORM = 'router:transform',
MIDDLEWARES = 'router:middlewares',
GUARDS = 'router:guards',
Expand Down
8 changes: 4 additions & 4 deletions lib/common/interfaces/context.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ export interface TransformContext {
query?: ServerRequest['query'];
body?: ServerRequest['body'];
headers?: ServerRequest['req']['headers'];
method: ServerRequest['req']['method'];
path: RouteDefinition['path'];
clientIp: ServerRequest['clientIp'];
method?: ServerRequest['req']['method'];
path?: RouteDefinition['path'];
clientIp?: ServerRequest['clientIp'];
userAgent?: string;
cookies?: Record<string, string>;
protocol: 'http' | 'https';
protocol?: 'http' | 'https';
referer?: string;
acceptedLanguages?: string[] | string;
}
Expand Down
10 changes: 9 additions & 1 deletion lib/common/interfaces/module.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ export interface ExistingProvider {
provide: InjectionToken;
useExisting: InjectionToken; // An alias for another provider
}
export type ImportableModule = Constructor<any> | DynamicModule;

export interface DynamicModule {
module: Constructor<any>;
providers?: Provider[];
exports?: InjectionToken[]; // Tokens to make available to other modules
imports?: ImportableModule[]; // Nested imports for the dynamic module
}
export interface ModuleMetadata {
controllers?: Constructor<any>[];

providers?: Provider[];
imports?: ImportableModule[]; // Other modules or dynamic modules imported
exports?: InjectionToken[];
}
4 changes: 2 additions & 2 deletions lib/common/types/app.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type ParsedBody = {
body: Record<string, any> | string | undefined | { [key: string]: any } | null;
bodyRaw: Buffer | undefined;
body: Record<string, any> | { [key: string]: any } | null;
bodyRaw: Buffer | null;
bodySize: number | string;
};
3 changes: 2 additions & 1 deletion lib/common/types/event.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CoreEventType, HttpStatus } from '../enums';
import { EventHandlers, RouteDefinition, ServerRequest } from '../interfaces';
export type EventListener<T extends CoreEventType> = { route: string; handler: EventHandler<T> } | { handler: EventHandler<T> };

export type EventHandlerMap = {
[CoreEventType.Start]: EventHandlers['StartHandler'];
Expand All @@ -24,5 +25,5 @@ export type CommonContextProps = {
timestamp?: Date;
statusMessage?: keyof typeof HttpStatus;
statusCodeClass?: '1xx' | '2xx' | '3xx' | '4xx' | '5xx' | 'Unknown';
ctx: ServerRequest;
ctx?: ServerRequest;
};
7 changes: 3 additions & 4 deletions lib/core/Application.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { IncomingMessage, ServerResponse, Server, createServer } from 'http';
import Reflector from '../metadata';
import { CoreModule } from './CoreModule';
import { Router } from '../router';
import { MiddlewareStack } from '../middleware';
import { setPoweredByHeader } from '../utils';
import { Context, ContextFactory } from '../context';
import { Injector } from '../decorator';
import { AppConfig, Constructor, LifecycleEvents, RouteDefinition } from '../common/interfaces';
import { CoreEventType, KEY_SETTINGS, ModuleMetadataKeys, RouterMetadataKeys } from '../common/enums';
import { AppConfigKey, AppConfigValue, EventHandler, EventType, GlobalCache, MiddlewareFn, Provider } from '../common/types';
import { AppConfig, Constructor, LifecycleEvents } from '../common/interfaces';
import { CoreEventType, KEY_SETTINGS } from '../common/enums';
import { AppConfigKey, AppConfigValue, EventHandler, EventType, GlobalCache, MiddlewareFn } from '../common/types';
export class Application {
private readonly coreModule: CoreModule;
private readonly router: Router;
Expand Down
33 changes: 30 additions & 3 deletions lib/decorator/Controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import { RouterMetadataKeys } from '../common/enums';
import Reflector from '../metadata';

import { RouteNormalizer, RouteValidation } from '../utils';
/**
* A decorator to define a controller with a route prefix.
* It combines the provided prefix with any existing controller prefix and ensures the final prefix is normalized.
* The decorator stores the full normalized prefix in the metadata for later use.
*
* @param prefix The prefix to be applied to the controller's route. It will be normalized.
* @returns A class decorator that defines the controller's route prefix.
*
* @example
* // Example of usage
* @Controller('/api')
* class ApiController {
* // This controller will have a base route of '/api'
* }
*
* @example
* // Example with combined prefix
* @Controller('/api')
* @Controller('/v1')
* class VersionedApiController {
* // This controller will have a base route of '/api/v1'
* }
*/
export function Controller(prefix: string): ClassDecorator {
return (target) => {
// Validate prefix using RouteValidation
if (!RouteValidation.isValidPath(prefix)) {
throw new Error(`Invalid route prefix: "${prefix}". Prefix cannot be empty or contain invalid characters.`);
}
const existingPrefix = Reflector.get(RouterMetadataKeys.CONTROLLER_PREFIX, target);
let fullPrefix = prefix;
let fullPrefix = RouteNormalizer.normalizePath(prefix);
if (existingPrefix) {
fullPrefix = `${existingPrefix}${prefix}`.replace(/\/+$/, '');
fullPrefix = RouteNormalizer.combinePaths(existingPrefix, prefix);
}
Reflector.define(RouterMetadataKeys.CONTROLLER_PREFIX, fullPrefix, target);
};
Expand Down
30 changes: 28 additions & 2 deletions lib/decorator/MultiLang.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
import { RouterMetadataKeys } from '../common/enums';
import { MultiLanguageContext } from '../common/interfaces';
import Reflector from '../metadata';

/**
* @module MultiLanguage
* @description
* The `@MultiLanguage` decorator is used to register multiple translations for a route.
* It automatically selects the language based on the 'accept-language' header of the request,
* providing the correct route or translation for that language.
*
* @param {MultiLanguageContext} translations - An object containing language mappings.
*
* @example
* ```typescript
* import { MultiLanguage } from './decorators/MultiLanguage';
* import { MultiLanguageContext } from './common/interfaces';
*
* class ExampleController {
* @MultiLanguage({
* en: '/foo', // English route
* fr: '/bar', // French route
* default: '/foo' // Default route
* })
* public handleRequest(ctx: ServerRequest) {
* console.log(`Selected language: ${ctx.language}`);
* ctx.end()
* }
* }
* ```
*/
export function MultiLanguage(translations: MultiLanguageContext): MethodDecorator {
return (target) => {
Reflector.define(RouterMetadataKeys.MULTI_LANG, translations, target.constructor);
Reflector.define(RouterMetadataKeys.MULTI_LANGUAGE, translations, target.constructor);
};
}
5 changes: 3 additions & 2 deletions lib/decorator/Transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ import Reflector from '../metadata';
* ctx.body.modified = true;
* }
* })
* public handleRequest() {
* public handleRequest(ctx: ServerRequest) {
* console.log('Request handled');
* ctx.res.end("Hello, World")
* }
* }
* ```
*/
export function Transform(transformFn: (ctx: TransformContext) => void): MethodDecorator {
return (target: any, propertyKey) => {
return (target, propertyKey) => {
Reflector.define(RouterMetadataKeys.TRANSFORM, transformFn, target.constructor, propertyKey);
};
}
77 changes: 66 additions & 11 deletions lib/decorator/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,55 @@ import { RequestMethod, RouterMetadataKeys } from '../common/enums';
import { RouteDefinition } from '../common/interfaces';
import { MiddlewareFn } from '../common/types';
import Reflector from '../metadata';
import { RouteNormalizer, RouteValidation } from '../utils';
/**
* @module Route Decorators
* @description
* This module provides decorators for HTTP methods (e.g., `@Get`, `@Post`, `@Put`, etc.).
* These decorators are used to define routes in the application and are mapped to the respective HTTP methods.
* They allow specifying the path for the route and optional middleware functions for request processing.
* The decorators also integrate with the `Reflector` to store route metadata that can be used later by the framework.
*
* The function `createDecorator` is a generator that creates HTTP method decorators like `@Get`, `@Post`, etc.
* Each of these decorators can be applied to controller methods to bind routes to HTTP requests.
*
* @example
* // Example of usage for @Get decorator:
* @Controller('/app')
* class AppController {
* @Get('/hello')
* getHello(ctx: ServerRequest) {
* ctx.send('Hello World!');
* }
* }
*
* @example
* // Example with middleware for @Post decorator:
* @Controller('/app')
* class AppController {
* @Post('/create', [routeMid])
* createResource(ctx: ServerRequest) {
* ctx.send('Resource Created');
* }
* }
*/
function createDecorator(method: RequestMethod) {
return (path: string, middlewares?: MiddlewareFn[]): MethodDecorator => {
return (target, __, descriptor) => {
if (!Object.values(RequestMethod).includes(method)) {
throw new Error(`Invalid RequestMethod: ${method}`);
}
// Validate prefix using RouteValidation
if (!RouteValidation.isValidPath(path)) {
throw new Error(`Invalid route prefix: "${path}". Prefix cannot be empty or contain invalid characters.`);
}
return (target, propertyKey, descriptor) => {
if (!descriptor || typeof descriptor.value !== 'function') {
throw new Error(`@${method} decorator must be applied to a method.`);
}
RouteNormalizer.validateMethod(target, propertyKey);
const controllerPrefix = Reflector.get(RouterMetadataKeys.CONTROLLER_PREFIX, target.constructor);
let fullPath = path;
if (controllerPrefix) {
fullPath = `${controllerPrefix}${path}`.replace(/\/+$/, '');
}
const normalizedPath = RouteNormalizer.normalizePath(path);
const fullPath = RouteNormalizer.combinePaths(controllerPrefix, normalizedPath);
const existingRoutes: RouteDefinition[] = Reflector.get(RouterMetadataKeys.ROUTES, target.constructor) ?? [];
existingRoutes.push({
method,
Expand All @@ -27,9 +65,26 @@ function createDecorator(method: RequestMethod) {
};
};
}
export function createMethodDecorators() {
return Object.values(RequestMethod).map((method: RequestMethod) => {
return createDecorator(method);
});
}
export const [Get, Post, Put, Delete, Patch, Options, Head, Search] = createMethodDecorators();

/**
* HTTP method decorators for common HTTP methods. These decorators can be applied to controller methods
* to map them to specific HTTP routes. Each decorator accepts a path and optional middleware functions.
*
* - @Get: Maps the method to a GET route.
* - @Post: Maps the method to a POST route.
* - @Put: Maps the method to a PUT route.
* - @Delete: Maps the method to a DELETE route.
* - @Patch: Maps the method to a PATCH route.
* - @Options: Maps the method to an OPTIONS route.
* - @Head: Maps the method to a HEAD route.
* - @Search: Maps the method to a SEARCH route.
*/
export const Get = createDecorator(RequestMethod.Get);
export const Post = createDecorator(RequestMethod.Post);
export const Put = createDecorator(RequestMethod.Put);
export const Delete = createDecorator(RequestMethod.Delete);
export const Patch = createDecorator(RequestMethod.Patch);
export const Options = createDecorator(RequestMethod.Options);
export const Head = createDecorator(RequestMethod.Head);
export const Search = createDecorator(RequestMethod.Search);
export const All = createDecorator(RequestMethod.All);
Loading

0 comments on commit d8cfaec

Please sign in to comment.