Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
PodaruDragos authored Nov 25, 2023
2 parents f7b3c46 + ebb986f commit 244305d
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 46 deletions.
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,22 +664,58 @@ class Service {
The `BaseMiddleware.bind()` method will bind the `TYPES.TraceIdValue` if it hasn't been bound yet or re-bind if it has
already been bound.
### Dealing with CORS
### Middleware decorators
You can use the `@withMiddleware()` decorator to register middleware on controllers and handlers. For example:
```typescript
function authenticate() {
return withMiddleware(
(req, res, next) => {
if (req.user === undefined) {
res.status(401).json({ errors: [ 'You must be logged in to access this resource.' ] })
}
next()
}
)
}

If you access a route from a browser and experience a CORS problem, in other words, your browser stops at the
OPTIONS request, you need to add a route for that method too. You need to write a method in your controller class to
handle the same route but for OPTIONS method, it can have empty body and no parameters though.
function authorizeRole(role: string) {
return withMiddleware(
(req, res, next) => {
if (!req.user.roles.includes(role)) {
res.status(403).json({ errors: [ 'Get out.' ] })
}
next()
}
)
}

```ts
@controller("/api/example")
class ExampleController extends BaseHttpController {
@httpGet("/:id")
public get(req: Request, res: Response) {
return {};
@controller('/api/user')
@authenticate()
class UserController {

@httpGet('/admin/:id')
@authorizeRole('ADMIN')
public getById(@requestParam('id') id: string) {
...
}
}
```
You can also decorate controllers and handlers with middleware using BaseMiddleware identitifers:
```typescript
class AuthenticationMiddleware extends BaseMiddleware {
handler(req, res, next) {
if (req.user === undefined) {
res.status(401).json({ errors: [ 'User is not logged in.' ] })
}
}
}

container.bind<BaseMiddleware>("AuthMiddleware").to(AuthenticationMiddleware)

@httpOptions("/:id")
public options() { }
@controller('/api/users')
@withMiddleware("AuthMiddleware")
class UserController {
...
}
```
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const METADATA_KEY = {
controllerMethod: 'inversify-express-utils:controller-method',
controllerParameter: 'inversify-express-utils:controller-parameter',
httpContext: 'inversify-express-utils:httpcontext',
middleware: 'inversify-express-utils:middleware',
};

export enum PARAMETER_TYPE {
Expand Down
8 changes: 4 additions & 4 deletions src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { RouteDetails, RouteInfo, RawMetadata } from './interfaces';

export function getRouteInfo(
container: inversifyInterfaces.Container,
): Array<RouteInfo> {
): RouteInfo[] {
const raw = getRawMetadata(container);

return raw.map(r => {
Expand All @@ -17,7 +17,7 @@ export function getRouteInfo(
const controllerPath = r.controllerMetadata.path;
const actionPath = m.path;
const paramMetadata = r.parameterMetadata;
let args: Array<string> | undefined;
let args: (string | undefined)[] | undefined = undefined;

if (paramMetadata !== undefined) {
const paramMetadataForKey = paramMetadata[m.key] || undefined;
Expand Down Expand Up @@ -66,7 +66,7 @@ export function getRouteInfo(
};

if (args) {
details.args = args;
details.args = args as string[];
}

return details;
Expand All @@ -81,7 +81,7 @@ export function getRouteInfo(

export function getRawMetadata(
container: inversifyInterfaces.Container
): Array<RawMetadata> {
): RawMetadata[] {
const controllers = getControllersFromContainer(container, true);

return controllers.map(controller => {
Expand Down
66 changes: 57 additions & 9 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
import { inject, injectable, decorate } from 'inversify';
import { TYPE, METADATA_KEY, PARAMETER_TYPE, HTTP_VERBS_ENUM, } from './constants';
import type { DecoratorTarget, Middleware, ControllerMetadata, HandlerDecorator, ControllerMethodMetadata, ControllerParameterMetadata, ParameterMetadata } from './interfaces';

import type { DecoratorTarget, Middleware, ControllerMetadata, HandlerDecorator, ControllerMethodMetadata, ControllerParameterMetadata, ParameterMetadata, MiddlewareMetaData } from './interfaces';
import { getMiddlewareMetadata, getOrCreateMetadata } from './utils';

export const injectHttpContext = inject(TYPE.HttpContext);

function defineMiddlewareMetadata(
target: DecoratorTarget,
metaDataKey: string,
...middleware: Middleware[]
): void {
// We register decorated middleware meteadata in a map, e.g. { "controller": [ middleware ] }
const middlewareMap: MiddlewareMetaData = getOrCreateMetadata(
METADATA_KEY.middleware, target,
{},
);

if (!(metaDataKey in middlewareMap)) {
middlewareMap[metaDataKey] = [];
}

middlewareMap[metaDataKey]?.push(...middleware);
Reflect.defineMetadata(METADATA_KEY.middleware, middlewareMap, target);
}

export function withMiddleware(...middleware: Middleware[]) {
return function (
target: DecoratorTarget | NewableFunction,
methodName?: string
): void {
if (methodName) {
defineMiddlewareMetadata(target, methodName, ...middleware);
} else if (isNewableFunction(target)) {
defineMiddlewareMetadata(
target.constructor,
target.name,
...middleware
);
}
};
}

function isNewableFunction(target: unknown): target is NewableFunction {
return typeof target === 'function' && target.prototype !== undefined;
}

export function controller(path: string, ...middleware: Middleware[]) {
return (target: NewableFunction): void => {
// Get the list of middleware registered with @middleware() decorators
const decoratedMiddleware = getMiddlewareMetadata(
target.constructor,
target.name
);

const currentMetadata: ControllerMetadata = {
middleware,
middleware: middleware.concat(decoratedMiddleware),
path,
target,
};
Expand All @@ -22,10 +68,10 @@ export function controller(path: string, ...middleware: Middleware[]) {
// We attach metadata to the Reflect object itself to avoid
// declaring additional globals. Also, the Reflect is available
// in both node and web browsers.
const previousMetadata: Array<ControllerMetadata> = Reflect.getMetadata(
const previousMetadata: ControllerMetadata[] = Reflect.getMetadata(
METADATA_KEY.controller,
Reflect,
) as Array<ControllerMetadata> || [];
) as ControllerMetadata[] || [];

const newMetadata = [currentMetadata, ...previousMetadata];

Expand Down Expand Up @@ -99,15 +145,17 @@ export function httpMethod(
...middleware: Middleware[]
): HandlerDecorator {
return (target: DecoratorTarget, key: string): void => {
const decoratedMiddleware = getMiddlewareMetadata(target, key);

const metadata: ControllerMethodMetadata = {
key,
method,
middleware,
middleware: middleware.concat(decoratedMiddleware),
path,
target,
};

let metadataList: Array<ControllerMethodMetadata> = [];
let metadataList: ControllerMethodMetadata[] = [];

if (
!Reflect.hasOwnMetadata(
Expand All @@ -124,7 +172,7 @@ export function httpMethod(
metadataList = Reflect.getOwnMetadata(
METADATA_KEY.controllerMethod,
target.constructor,
) as Array<ControllerMethodMetadata>;
) as ControllerMethodMetadata[];
}

metadataList.push(metadata);
Expand Down Expand Up @@ -183,7 +231,7 @@ export function params(
index: number
): void => {
let metadataList: ControllerParameterMetadata = {};
let parameterMetadataList: Array<ParameterMetadata> = [];
let parameterMetadataList: ParameterMetadata[] = [];
const parameterMetadata: ParameterMetadata = {
index,
injectRoot: parameterName === undefined,
Expand Down
21 changes: 12 additions & 9 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Prototype<T> = {
};

interface ConstructorFunction<T = Record<string, unknown>> {
new(...args: Array<unknown>): T;
new(...args: unknown[]): T;
prototype: Prototype<T>;
}

Expand All @@ -19,12 +19,15 @@ export type DecoratorTarget<T = unknown> =

export type Middleware = (string | symbol | RequestHandler);

export interface MiddlewareMetaData {
[identifier: string]: Middleware[];
}

export type ControllerHandler = (...params: Array<unknown>) => unknown;
export type ControllerHandler = (...params: unknown[]) => unknown;
export type Controller = Record<string, ControllerHandler>;

export interface ControllerMetadata {
middleware: Array<Middleware>;
middleware: Middleware[];
path: string;
target: DecoratorTarget;
}
Expand All @@ -35,7 +38,7 @@ export interface ControllerMethodMetadata extends ControllerMetadata {
}

export interface ControllerParameterMetadata {
[methodName: string]: Array<ParameterMetadata>;
[methodName: string]: ParameterMetadata[];
}

export interface ParameterMetadata {
Expand All @@ -46,9 +49,9 @@ export interface ParameterMetadata {
}

export type ExtractedParameters =
| Array<ParameterMetadata>
| ParameterMetadata[]
| [Request, Response, NextFunction]
| Array<unknown>
| unknown[]

export type HandlerDecorator = (
target: DecoratorTarget,
Expand Down Expand Up @@ -91,17 +94,17 @@ export interface IHttpActionResult {
}

export interface RouteDetails {
args?: Array<string>;
args?: string[];
route: string;
}

export interface RouteInfo {
controller: string;
endpoints: Array<RouteDetails>;
endpoints: RouteDetails[];
}

export interface RawMetadata {
controllerMetadata: ControllerMetadata,
methodMetadata: Array<ControllerMethodMetadata>,
methodMetadata: ControllerMethodMetadata[],
parameterMetadata: ControllerParameterMetadata,
}
40 changes: 31 additions & 9 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { interfaces } from 'inversify';
import { METADATA_KEY, NO_CONTROLLERS_FOUND, TYPE } from './constants';
import type { Controller, ControllerMetadata, ControllerMethodMetadata, ControllerParameterMetadata, DecoratorTarget, IHttpActionResult } from './interfaces';
import type { Controller, ControllerMetadata, ControllerMethodMetadata, ControllerParameterMetadata, DecoratorTarget, IHttpActionResult, Middleware, MiddlewareMetaData } from './interfaces';

export function getControllersFromContainer(
container: interfaces.Container,
forceControllers: boolean,
): Array<Controller> {
): Controller[] {
if (container.isBound(TYPE.Controller)) {
return container.getAll<Controller>(TYPE.Controller);
} if (forceControllers) {
Expand All @@ -15,19 +15,28 @@ export function getControllersFromContainer(
}
}

export function getControllersFromMetadata(): Array<DecoratorTarget> {
const arrayOfControllerMetadata: Array<ControllerMetadata> =
export function getControllersFromMetadata(): DecoratorTarget[] {
const arrayOfControllerMetadata: ControllerMetadata[] =
Reflect.getMetadata(
METADATA_KEY.controller,
Reflect,
) as Array<ControllerMetadata> || [];
) as ControllerMetadata[] || [];
return arrayOfControllerMetadata.map(metadata => metadata.target);
}

export function getMiddlewareMetadata(constructor: DecoratorTarget, key: string)
: Middleware[] {
const middlewareMetadata = Reflect.getMetadata(
METADATA_KEY.middleware,
constructor
) as MiddlewareMetaData || {};
return middlewareMetadata[key] || [];
}

export function getControllerMetadata(
constructor: NewableFunction
): ControllerMetadata {
const controllerMetadata: ControllerMetadata = Reflect.getOwnMetadata(
const controllerMetadata: ControllerMetadata = Reflect.getMetadata(
METADATA_KEY.controller,
constructor,
) as ControllerMetadata;
Expand All @@ -36,16 +45,16 @@ export function getControllerMetadata(

export function getControllerMethodMetadata(
constructor: NewableFunction,
): Array<ControllerMethodMetadata> {
): ControllerMethodMetadata[] {
const methodMetadata = Reflect.getOwnMetadata(
METADATA_KEY.controllerMethod,
constructor,
) as Array<ControllerMethodMetadata>;
) as ControllerMethodMetadata[];

const genericMetadata = Reflect.getMetadata(
METADATA_KEY.controllerMethod,
Reflect.getPrototypeOf(constructor) as NewableFunction,
) as Array<ControllerMethodMetadata>;
) as ControllerMethodMetadata[];

if (genericMetadata !== undefined && methodMetadata !== undefined) {
return methodMetadata.concat(genericMetadata);
Expand Down Expand Up @@ -90,3 +99,16 @@ export function instanceOfIHttpActionResult(
return value != null &&
typeof (value as IHttpActionResult).executeAsync === 'function';
}

export function getOrCreateMetadata<T>(
key: string,
target: object,
defaultValue: T
): T {
if (!Reflect.hasMetadata(key, target)) {
Reflect.defineMetadata(key, defaultValue, target);
return defaultValue;
}

return Reflect.getMetadata(key, target) as T;
}
2 changes: 1 addition & 1 deletion test/base_middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('BaseMiddleware', () => {
name: string;
}

const logEntries: Array<string> = [];
const logEntries: string[] = [];

@injectable()
class LoggerMiddleware extends BaseMiddleware {
Expand Down
Loading

0 comments on commit 244305d

Please sign in to comment.