Skip to content

feat: include the current request context when resolving controller from IoC #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,39 @@ export class UsersController {
}
```

For other IoC providers that don't expose a `get(xxx)` function, you can create an IoC adapter using `IocAdapter` like so:

```typescript
// inversify-adapter.ts
import { IoCAdapter } from 'routing-controllers'
import { Container } from 'inversify'

class InversifyAdapter implements IocAdapter {
constructor (
private readonly container: Container
) {
}

get<T> (someClass: ClassConstructor<T>, action?: Action): T {
const childContainer = this.container.createChild()
childContainer.bind(API_SYMBOLS.ClientIp).toConstantValue(action.context.ip)
return childContainer.resolve<T>(someClass)
}
}
```

And then tell Routing Controllers to use it:
```typescript
// Somewhere in your app startup
import { useContainer } from 'routing-controllers'
import { Container } from 'inversify'
import { InversifyAdapter } from './inversify-adapter.ts'

const container = new Container()
const inversifyAdapter = new InversifyAdapter(container)
useContainer(inversifyAdapter)
```

## Custom parameter decorators

You can create your own parameter decorators.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "routing-controllers",
"private": true,
"name": "@gritcode/routing-controllers",
"private": false,
"version": "0.8.0",
"description": "Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage for Express / Koa using TypeScript.",
"license": "MIT",
Expand Down Expand Up @@ -38,6 +38,10 @@
"reflect-metadata": "^0.1.13",
"template-url": "^1.0.0"
},
"peerDependencies": {
"class-transformer": "^0.2.3",
"class-validator": "0.10.1"
},
"devDependencies": {
"@types/chai": "^4.2.3",
"@types/chai-as-promised": "7.1.2",
Expand Down Expand Up @@ -82,10 +86,6 @@
"typedi": "~0.8.0",
"typescript": "~3.6.3"
},
"peerDependencies": {
"class-transformer": "^0.2.3",
"class-validator": "0.10.1"
},
"scripts": {
"build": "rimraf build && echo Using TypeScript && tsc --version && tsc --pretty",
"clean": "rimraf build coverage",
Expand Down
5 changes: 2 additions & 3 deletions src/RoutingControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export class RoutingControllers<T extends BaseDriver> {
* Executes given controller action.
*/
protected executeAction(actionMetadata: ActionMetadata, action: Action, interceptorFns: Function[]) {

// compute all parameters
const paramsPromises = actionMetadata.params
.sort((param1, param2) => param1.index - param2.index)
Expand All @@ -120,7 +119,7 @@ export class RoutingControllers<T extends BaseDriver> {

// execute action and handle result
const allParams = actionMetadata.appendParams ? actionMetadata.appendParams(action).concat(params) : params;
const result = actionMetadata.methodOverride ? actionMetadata.methodOverride(actionMetadata, action, allParams) : actionMetadata.callMethod(allParams);
const result = actionMetadata.methodOverride ? actionMetadata.methodOverride(actionMetadata, action, allParams) : actionMetadata.callMethod(allParams, action);
return this.handleCallMethodResult(result, actionMetadata, action, interceptorFns);

}).catch(error => {
Expand Down Expand Up @@ -172,7 +171,7 @@ export class RoutingControllers<T extends BaseDriver> {
return uses.map(use => {
if (use.interceptor.prototype && use.interceptor.prototype.intercept) { // if this is function instance of InterceptorInterface
return function (action: Action, result: any) {
return (getFromContainer(use.interceptor) as InterceptorInterface).intercept(action, result);
return (getFromContainer(use.interceptor, action) as InterceptorInterface).intercept(action, result);
};
}
return use.interceptor;
Expand Down
35 changes: 28 additions & 7 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Action } from "./Action";

/**
* Container options.
Expand All @@ -16,13 +17,15 @@ export interface UseContainerOptions {

}

export type ClassConstructor<T> = { new (...args: any[]): T };

/**
* Container to be used by this library for inversion control. If container was not implicitly set then by default
* container simply creates a new instance of the given class.
*/
const defaultContainer: { get<T>(someClass: { new (...args: any[]): T }|Function): T } = new (class {
const defaultContainer: { get<T>(someClass: ClassConstructor<T> | Function): T } = new (class {
private instances: { type: Function, object: any }[] = [];
get<T>(someClass: { new (...args: any[]): T }): T {
get<T>(someClass: ClassConstructor<T>): T {
let instance = this.instances.find(instance => instance.type === someClass);
if (!instance) {
instance = { type: someClass, object: new someClass() };
Expand All @@ -33,24 +36,42 @@ const defaultContainer: { get<T>(someClass: { new (...args: any[]): T }|Function
}
})();

let userContainer: { get<T>(someClass: { new (...args: any[]): T }|Function): T };
let userContainer: { get<T>(
someClass: ClassConstructor<T> | Function,
action?: Action
): T };
let userContainerOptions: UseContainerOptions;

/**
* Allows routing controllers to resolve objects using your IoC container
*/
export interface IocAdapter {
/**
* Return
*/
get<T> (someClass: ClassConstructor<T>, action?: Action): T;
}

/**
* Sets container to be used by this library.
*/
export function useContainer(iocContainer: { get(someClass: any): any }, options?: UseContainerOptions) {
userContainer = iocContainer;
export function useContainer(iocAdapter: IocAdapter, options?: UseContainerOptions) {
userContainer = iocAdapter;
userContainerOptions = options;
}

/**
* Gets the IOC container used by this library.
* @param someClass A class constructor to resolve
* @param action The request/response context that `someClass` is being resolved for
*/
export function getFromContainer<T>(someClass: { new (...args: any[]): T }|Function): T {
export function getFromContainer<T>(
someClass: ClassConstructor<T> | Function,
action?: Action
): T {
if (userContainer) {
try {
const instance = userContainer.get(someClass);
const instance = userContainer.get(someClass, action);
if (instance)
return instance;

Expand Down
4 changes: 2 additions & 2 deletions src/driver/koa/KoaDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class KoaDriver extends BaseDriver {
const action: Action = { request: context.request, response: context.response, context, next };
try {
const checkResult = actionMetadata.authorizedRoles instanceof Function ?
getFromContainer<RoleChecker>(actionMetadata.authorizedRoles).check(action) :
getFromContainer<RoleChecker>(actionMetadata.authorizedRoles, action).check(action) :
this.authorizationChecker(action, actionMetadata.authorizedRoles);

const handleError = (result: any) => {
Expand Down Expand Up @@ -331,7 +331,7 @@ export class KoaDriver extends BaseDriver {
if (use.middleware.prototype && use.middleware.prototype.use) { // if this is function instance of MiddlewareInterface
middlewareFunctions.push((context: any, next: (err?: any) => Promise<any>) => {
try {
const useResult = (getFromContainer(use.middleware) as KoaMiddlewareInterface).use(context, next);
const useResult = (getFromContainer(use.middleware, { context } as Action) as KoaMiddlewareInterface).use(context, next);
if (isPromiseLike(useResult)) {
useResult.catch((error: any) => {
this.handleError(error, undefined, {
Expand Down
4 changes: 2 additions & 2 deletions src/metadata/ActionMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ export class ActionMetadata {
* Calls action method.
* Action method is an action defined in a user controller.
*/
callMethod(params: any[]) {
const controllerInstance = this.controllerMetadata.instance;
callMethod(params: any[], action: Action) {
const controllerInstance = this.controllerMetadata.getInstance(action);
return controllerInstance[this.method].apply(controllerInstance, params);
}

Expand Down
6 changes: 4 additions & 2 deletions src/metadata/ControllerMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {UseMetadata} from "./UseMetadata";
import {getFromContainer} from "../container";
import {ResponseHandlerMetadata} from "./ResponseHandleMetadata";
import {InterceptorMetadata} from "./InterceptorMetadata";
import { Action } from "../Action";

/**
* Controller metadata.
Expand Down Expand Up @@ -70,9 +71,10 @@ export class ControllerMetadata {

/**
* Gets instance of the controller.
* @param action Details around the request session
*/
get instance(): any {
return getFromContainer(this.target);
getInstance(action: Action): any {
return getFromContainer(this.target, action);
}

// -------------------------------------------------------------------------
Expand Down
26 changes: 20 additions & 6 deletions test/functional/container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import "reflect-metadata";
import {JsonController} from "../../src/decorator/JsonController";
import {createExpressServer, createKoaServer, getMetadataArgsStorage} from "../../src/index";
import {createExpressServer, createKoaServer, getMetadataArgsStorage, Action} from "../../src/index";
import {assertRequest} from "./test-utils";
import {Container, Service} from "typedi";
import {useContainer} from "../../src/container";
import {useContainer, IocAdapter, ClassConstructor} from "../../src/container";
import {Get} from "../../src/decorator/Get";
import * as assert from "assert";
const chakram = require("chakram");
const expect = chakram.expect;

Expand Down Expand Up @@ -104,17 +105,26 @@ describe("container", () => {

describe("using custom container should be possible", () => {

let fakeContainer: IocAdapter & {
services: { [key: string]: any }
context: any[]
};

before(() => {

const fakeContainer = {
services: [] as any,
fakeContainer = {
services: {},
context: [],

get<T>(service: ClassConstructor<T>, action: Action): T {

get(service: any) {
this.context.push(action.context);

if (!this.services[service.name]) {
this.services[service.name] = new service();
}

return this.services[service.name];
return this.services[service.name] as T;
}
};

Expand Down Expand Up @@ -206,6 +216,10 @@ describe("container", () => {
title: "post #2"
}]);
});

it("should pass the action through to the Ioc adapter", () => {
assert.notEqual(fakeContainer.context.length, 0);
});
});

describe("using custom container with fallback should be possible", () => {
Expand Down