Skip to content

Commit

Permalink
feat: Execution context to get handler and class
Browse files Browse the repository at this point in the history
  • Loading branch information
Sorikairox committed Apr 8, 2023
1 parent f17490b commit 765d5e2
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 29 deletions.
80 changes: 80 additions & 0 deletions doc/fundamentals/execution-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

`ExecutionContext` provides additional details about the current execution process. Danet provides an instance of `ExecutionContext` in places you may need it, such as in the `canActivate()` method of a [guard](https://savory.github.io/Danet/overview/guards/) and the `action()` method of a [middleware](https://savory.github.io/Danet/overview/middlewares/). It provides the following methods:

```ts
type ExecutionContext = {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass(): Constructor;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
```
The `getHandler()` method returns a reference to the handler about to be invoked. The `getClass()` method returns the type of the `Controller` class which this particular handler belongs to. For example, if the currently processed request is a `POST` request, bound to the `create()` method on the `TodoController`, `getHandler()` returns a reference to the `create()` method and `getClass()` returns the `TodoController` **type** (not instance).
```typescript
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "TodoController"
```

The ability to access references to both the current class and handler method provides great flexibility. Most importantly, it gives us the opportunity to access the metadata set through the `@SetMetadata()` decorator from within guards or interceptors. We cover this use case below.

#### Reflection and metadata

Danet provides the ability to attach **custom metadata** to route handlers through the `@SetMetadata()` decorator. We can then access this metadata from within our class to make certain decisions.

```ts todo.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createTodoDto: CreateTodoDto) {
this.todoService.create(createCatDto);
}

```

With the construction above, we attached the `roles` metadata (`roles` is a metadata key and `['admin']` is the associated value) to the `create()` method. While this works, it's not good practice to use `@SetMetadata()` directly in your routes. Instead, create your own decorators, as shown below:

```typescript roles.decorators.ts
import { SetMetadata } from 'https://deno.land/x/danet/mod.ts';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
```

This approach is much cleaner and more readable, and is strongly typed. Now that we have a custom `@Roles()` decorator, we can use it to decorate the `create()` method.

```typescript todo.controller.ts
@Post()
@Roles('admin')
async create(@Body() createTodoDto: CreateTodoDto) {
this.todoService.create(createCatDto);
}
```

To access the route's role(s) (custom metadata), we'll use the `MetadataHelper` static methods,

To read the handler metadata, use the `get()` method.

```typescript
const roles = MetadataHelper.getMetadata<string[]>('roles', context.getHandler());
```

The `MetadataHelper#getMetadata` method allows us to easily access the metadata by passing in two arguments: a metadata **key** and a **context** (decorator target) to retrieve the metadata from. In this example, the specified **key** is `'roles'` (refer back to the `roles.decorator.ts` file above and the `SetMetadata()` call made there). The context is provided by the call to `context.getHandler()`, which results in extracting the metadata for the currently processed route handler. Remember, `getHandler()` gives us a **reference** to the route handler function.

Alternatively, we may organize our controller by applying metadata at the controller level, applying to all routes in the controller class.

```typescript todo.controller.ts
@Roles('admin')
@Controller('todo')
export class TodoController {}
```

In this case, to extract controller metadata, we pass `context.getClass()` as the second argument (to provide the controller class as the context for metadata extraction) instead of `context.getHandler()`:

```typescript roles.guard.ts
const roles = MetadataHelper.getMetadata<string[]>('roles', context.getClass());
```
13 changes: 7 additions & 6 deletions doc/overview/guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ As mentioned, **authorization** is a great use case for Guards because specific
```typescript simple-auth-guard.ts

import { Injectable, AuthGuard } from 'https://deno.land/x/danet/mod.ts';
import { ExecutionContext } from "./router.ts";

@Injectable()
export class SimpleAuthGuard implements AuthGuard {
canActivate(
context: HttpContext,
): boolean | Promise<boolean> {
const request = context.request;
return validateRequest(request);
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> {
const request = context.request;
return validateRequest(request);
}
}
```

Expand Down
19 changes: 15 additions & 4 deletions spec/auth-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { TokenInjector } from '../src/injector/injectable/constructor.ts';
import { Injectable } from '../src/injector/injectable/decorator.ts';
import { Module } from '../src/module/decorator.ts';
import { Controller, Get } from '../src/router/controller/decorator.ts';
import { HttpContext } from '../src/router/router.ts';
import { ExecutionContext, HttpContext } from '../src/router/router.ts';
import { SetMetadata } from "../src/metadata/decorator.ts";
import { MetadataHelper } from "../src/metadata/helper.ts";

@Injectable()
class SimpleService {
Expand All @@ -22,7 +24,7 @@ class GlobalGuard implements AuthGuard {
constructor(private simpleService: SimpleService) {
}

canActivate(context: HttpContext) {
canActivate(context: ExecutionContext) {
this.simpleService.doSomething();
context.response.body = {
passedInglobalGuard: true,
Expand All @@ -36,10 +38,13 @@ class ControllerGuard implements AuthGuard {
constructor(private simpleService: SimpleService) {
}

canActivate(context: HttpContext) {
canActivate(context: ExecutionContext) {
const controller = context.getClass();
const customMetadata = MetadataHelper.getMetadata('customMetadata', controller);
this.simpleService.doSomething();
context.response.body = {
passedIncontrollerGuard: true,
customMetadata
};
return true;
}
Expand All @@ -50,22 +55,27 @@ class MethodGuard implements AuthGuard {
constructor(private simpleService: SimpleService) {
}

canActivate(context: HttpContext) {
canActivate(context: ExecutionContext) {
this.simpleService.doSomething();
const method = context.getHandler();
const customMetadata = MetadataHelper.getMetadata('customMetadata', method);
context.response.body = {
passedInmethodGuard: true,
customMetadata,
};
return true;
}
}

@Controller('method-guard')
class MethodGuardController {
@SetMetadata('customMetadata', 'customValue')
@UseGuard(MethodGuard)
@Get('/')
simpleGet() {}
}

@SetMetadata('customMetadata', 'customValue')
@UseGuard(ControllerGuard)
@Controller('controller-guard')
class AuthGuardController {
Expand Down Expand Up @@ -100,6 +110,7 @@ for (const guardType of ['controller', 'method']) {
const json = await res.json();
assertEquals(json, {
[`passedIn${guardType}Guard`]: true,
customMetadata: 'customValue'
});
await app.close();
});
Expand Down
10 changes: 5 additions & 5 deletions spec/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DanetApplication } from '../src/app.ts';
import { Module } from '../src/module/decorator.ts';
import { Controller, Get } from '../src/router/controller/decorator.ts';
import { Injectable } from '../src/injector/injectable/decorator.ts';
import { HttpContext } from '../src/router/router.ts';
import { ExecutionContext, HttpContext } from '../src/router/router.ts';
import {
DanetMiddleware,
Middleware,
Expand All @@ -27,7 +27,7 @@ class SimpleMiddleware implements DanetMiddleware {
constructor(private simpleInjectable: SimpleInjectable) {
}

async action(ctx: HttpContext, next: NextFunction) {
async action(ctx: ExecutionContext, next: NextFunction) {
ctx.response.body = `${ctx.response.body as string || ''}` +
this.simpleInjectable.doSomething();
await next();
Expand All @@ -39,7 +39,7 @@ class ThrowingMiddleware implements DanetMiddleware {
constructor(private simpleInjectable: SimpleInjectable) {
}

action(ctx: HttpContext) {
action(ctx: ExecutionContext) {
throw new BadRequestException();
}
}
Expand Down Expand Up @@ -125,7 +125,7 @@ Deno.test('Middleware controller decorator', async () => {

@Injectable()
class FirstGlobalMiddleware implements DanetMiddleware {
async action(ctx: HttpContext, next: NextFunction) {
async action(ctx: ExecutionContext, next: NextFunction) {
ctx.response.body = `${
ctx.response.body as string || ''
}[first-middleware]`;
Expand All @@ -135,7 +135,7 @@ class FirstGlobalMiddleware implements DanetMiddleware {

@Injectable()
class SecondGlobalMiddleware implements DanetMiddleware {
async action(ctx: HttpContext, next: NextFunction) {
async action(ctx: ExecutionContext, next: NextFunction) {
ctx.response.body = `${
ctx.response.body as string || ''
}[second-middleware]`;
Expand Down
12 changes: 6 additions & 6 deletions src/guard/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ForbiddenException } from '../exception/http/mod.ts';
import { Injector } from '../injector/injector.ts';
import { MetadataHelper } from '../metadata/helper.ts';
import { ControllerConstructor } from '../router/controller/constructor.ts';
import { Callback, HttpContext } from '../router/router.ts';
import { ExecutionContext, Callback, HttpContext } from '../router/router.ts';
import { Constructor } from '../utils/constructor.ts';
import { GLOBAL_GUARD } from './constants.ts';
import { guardMetadataKey } from './decorator.ts';
Expand All @@ -13,7 +13,7 @@ export class GuardExecutor {
}

async executeAllRelevantGuards(
context: HttpContext,
context: ExecutionContext,
Controller: ControllerConstructor,
ControllerMethod: Callback,
) {
Expand All @@ -25,23 +25,23 @@ export class GuardExecutor {
);
}

async executeGlobalGuard(context: HttpContext) {
async executeGlobalGuard(context: ExecutionContext) {
if (this.injector.has(GLOBAL_GUARD)) {
const globalGuard: AuthGuard = await this.injector.get(GLOBAL_GUARD);
await this.executeGuard(globalGuard, context);
}
}

async executeControllerAndMethodAuthGuards(
context: HttpContext,
context: ExecutionContext,
Controller: ControllerConstructor,
ControllerMethod: Callback,
) {
await this.executeGuardFromMetadata(context, Controller);
await this.executeGuardFromMetadata(context, ControllerMethod);
}

async executeGuard(guard: AuthGuard, context: HttpContext) {
async executeGuard(guard: AuthGuard, context: ExecutionContext) {
if (guard) {
const canActivate = await guard.canActivate(context);
if (!canActivate) {
Expand All @@ -51,7 +51,7 @@ export class GuardExecutor {
}

async executeGuardFromMetadata(
context: HttpContext,
context: ExecutionContext,
// deno-lint-ignore ban-types
constructor: Constructor | Function,
) {
Expand Down
4 changes: 2 additions & 2 deletions src/guard/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpContext } from '../router/router.ts';
import { ExecutionContext } from '../router/router.ts';

export interface AuthGuard {
canActivate(context: HttpContext): Promise<boolean> | boolean;
canActivate(context: ExecutionContext): Promise<boolean> | boolean;
}
4 changes: 2 additions & 2 deletions src/router/middleware/executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injector } from '../../injector/injector.ts';
import { Callback, HttpContext } from '../router.ts';
import { Callback, ExecutionContext, HttpContext } from '../router.ts';
import { ControllerConstructor } from '../controller/constructor.ts';
import { InjectableConstructor } from '../../injector/injectable/constructor.ts';
import { MetadataHelper } from '../../metadata/helper.ts';
Expand All @@ -19,7 +19,7 @@ export class MiddlewareExecutor {
}

async executeAllRelevantMiddlewares(
context: HttpContext,
context: ExecutionContext,
Controller: ControllerConstructor,
ControllerMethod: Callback,
next: NextFunction,
Expand Down
18 changes: 14 additions & 4 deletions src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export type Callback = (...args: any[]) => unknown;

export type HttpContext = Context;

export type ExecutionContext = HttpContext & {
// deno-lint-ignore ban-types
getHandler: () => Function,
getClass: () => Constructor
};

export class DanetRouter {
public router = new Router();
private logger: Logger = new Logger('Router');
Expand Down Expand Up @@ -99,13 +105,18 @@ export class DanetRouter {
) {
return async (context: HttpContext) => {
try {
context = {
...context,
getClass: () => Controller,
getHandler: () => ControllerMethod,
} as unknown as ExecutionContext;
await this.middlewareExecutor.executeAllRelevantMiddlewares(
context,
context as ExecutionContext,
Controller,
ControllerMethod,
async () => {
await this.guardExecutor.executeAllRelevantGuards(
context,
context as ExecutionContext,
Controller,
ControllerMethod,
);
Expand Down Expand Up @@ -175,8 +186,7 @@ export class DanetRouter {
Controller: ControllerConstructor,
// deno-lint-ignore no-explicit-any
ControllerMethod: (...args: any[]) => unknown,
// deno-lint-ignore no-explicit-any
context: Context<State, Record<string, any>>,
context: HttpContext,
) {
const paramResolverMap: Map<number, Resolver> = MetadataHelper.getMetadata(
argumentResolverFunctionsMetadataKey,
Expand Down

0 comments on commit 765d5e2

Please sign in to comment.