Inversifyjs: HTTP methods from parent class

Created on 26 Jun 2017  路  12Comments  路  Source: inversify/InversifyJS

Expected Behavior


abstract class AbstractRestController<T> {
    protected repository: Repository<T>;

    // Fixed from @Get('/id') 
    @Get('/:id') 
    async getById(@RequestParam('id') id: string): Promise<T> {
        return await repository.findOneById(id);
    }
}

@Controller('/rest')
class RestController extends AbstractRestController<SomeModel> {
    constructor(@inject('some-repository') repository: Repository<SomeModel>) {
        super(repository);
    }
}

Request to /rest/1 returns error page with 404 code and 'Cannot GET /rest' text .

Current Behavior



HTTP methods from parent classes not registered in Express.

Your Environment

  • inversify version used: 4.1.1
  • inversify-express-utils version used: 3.5.2

Most helpful comment

@oneassasin @remojansen Here's a workaround I made for my own controllers to inherit from a parent. When I get time I'll put this into a PR.

function mergeInheritedControllers() {
    function getControllersFromMetadata(): Function[] {
        const arrayOfControllerMetadata: interfaces.ControllerMetadata[] = Reflect.getMetadata(
            "inversify-express-utils:controller", Reflect
        ) || [];

        return arrayOfControllerMetadata.map((metadata) => metadata.target);
    }

    getControllersFromMetadata().forEach(controller => {
        const methods = mergeInheritedMethods(controller);
        Reflect.defineMetadata("inversify-express-utils:controller-method", methods, controller);

        const parameters = mergeInheritedParameters(controller);
        Reflect.defineMetadata("inversify-express-utils:controller-parameter", parameters, controller);
    });
}

function mergeInheritedMethods(controller: Function): interfaces.ControllerMethodMetadata[] {
    function getControllerMethodMetadata(constructor: Function): interfaces.ControllerMethodMetadata[] {
        return Reflect.getOwnMetadata("inversify-express-utils:controller-method", constructor);
    }

    const methods = [].concat(getControllerMethodMetadata(controller) || []);
    if (controller.prototype.__proto__ !== Object.prototype) {
        const inherited = mergeInheritedMethods(controller.prototype.__proto__.constructor);

        for (const item of inherited) {
            if (isObject(item)) {
                if (methods.filter(map => isObject(map) && map.key === item.key).length === 0) {
                    methods.push(Object.assign({}, item, {target: controller.prototype}));
                }
            }
        }
    }

    return methods;
}

function mergeInheritedParameters(controller) {
    function getControllerParameterMetadata(constructor: any): interfaces.ControllerParameterMetadata {
        return Reflect.getOwnMetadata("inversify-express-utils:controller-parameter", constructor);
    }

    let parameters = Object.assign({}, getControllerParameterMetadata(controller));
    if (controller.prototype.__proto__ !== Object.prototype) {
        const inherited = mergeInheritedParameters(controller.prototype.__proto__.constructor);
        parameters = Object.assign({}, inherited, parameters);
    }

    return parameters;
}

All 12 comments

Just took a quick look at the code.
In the abstract class you use @Get('/id') but this needs to be @Get('/:id).
If you don麓t add this the route would be /rest/id

It`s just example :)
The problem is this: in the method for installing controllers, there is no search in the class prototype

Also, for using Get, Post, Update and Delete decorators class need to declare class with injectable decorator. Maybe, need to add checking if class is abstract, skip this condition?

Can I just ask if you wrote those decorators, or if they come from express?

What decorators are you meant?
@Controller, @Get? Its decorators from inversify-express-utils. Now their names @controller and @httpGet.

@robertmain we wrote the decorators they don't come express. They come from https://github.com/inversify/inversify-express-utils

@oneassasin just an update on this. I want to implement generic controllers myself so I will look at this feature at some point. It is not an easy one because the way things work internally but I will try my best to solve it. Unfortunately, it will take me some time because I try to solve simple issues first.

Thanks for suggesting this feature 馃憤

@oneassasin @remojansen Here's a workaround I made for my own controllers to inherit from a parent. When I get time I'll put this into a PR.

function mergeInheritedControllers() {
    function getControllersFromMetadata(): Function[] {
        const arrayOfControllerMetadata: interfaces.ControllerMetadata[] = Reflect.getMetadata(
            "inversify-express-utils:controller", Reflect
        ) || [];

        return arrayOfControllerMetadata.map((metadata) => metadata.target);
    }

    getControllersFromMetadata().forEach(controller => {
        const methods = mergeInheritedMethods(controller);
        Reflect.defineMetadata("inversify-express-utils:controller-method", methods, controller);

        const parameters = mergeInheritedParameters(controller);
        Reflect.defineMetadata("inversify-express-utils:controller-parameter", parameters, controller);
    });
}

function mergeInheritedMethods(controller: Function): interfaces.ControllerMethodMetadata[] {
    function getControllerMethodMetadata(constructor: Function): interfaces.ControllerMethodMetadata[] {
        return Reflect.getOwnMetadata("inversify-express-utils:controller-method", constructor);
    }

    const methods = [].concat(getControllerMethodMetadata(controller) || []);
    if (controller.prototype.__proto__ !== Object.prototype) {
        const inherited = mergeInheritedMethods(controller.prototype.__proto__.constructor);

        for (const item of inherited) {
            if (isObject(item)) {
                if (methods.filter(map => isObject(map) && map.key === item.key).length === 0) {
                    methods.push(Object.assign({}, item, {target: controller.prototype}));
                }
            }
        }
    }

    return methods;
}

function mergeInheritedParameters(controller) {
    function getControllerParameterMetadata(constructor: any): interfaces.ControllerParameterMetadata {
        return Reflect.getOwnMetadata("inversify-express-utils:controller-parameter", constructor);
    }

    let parameters = Object.assign({}, getControllerParameterMetadata(controller));
    if (controller.prototype.__proto__ !== Object.prototype) {
        const inherited = mergeInheritedParameters(controller.prototype.__proto__.constructor);
        parameters = Object.assign({}, inherited, parameters);
    }

    return parameters;
}

@rmblstrp a PR would be awesome :+1:

Hello,
I have the same issue but with MongoClient. My goal would be to have something like this
------ generic-client.ts ------

import { Db, ObjectID } from 'mongodb';
import { injectable } from 'inversify';
import { MongoDBConnection } from '../services/database.service';

@injectable()
export class GenericClient {
    public db: Db;

    constructor() {
        MongoDBConnection.getConnection((connection) => {
            this.db = connection;
        });
    }

    public find(collection: string, filter: Object, result: (error: Error, data: any) => void): void {
       return this.db.collection(collection).find(filter).toArray((error, find) => {
            return result(error, find);
        });
    }

    public findOneById(collection: string, objectId: string, result: (error: Error, data: any) => void): void {
        return this.db.collection(collection).find({ _id: new ObjectID(objectId) }).limit(1).toArray((error, find) => {
            return result(error, find[0]);
        });
    }

    public insert(collection: string, model: any, result: (error: Error, data: any) => void): void {
        this.db.collection(collection).insertOne(model, (error, insert) => {
            return result(error, insert.ops[0]);
        });
    }

    public update(collection: string, objectId: string, model: any, result: (error: Error, data: any) => void): void {
        this.db.collection(collection).updateOne(
            { _id: new ObjectID(objectId) },
            { $set: model },
            (error, update) => result(error, model)
        );
    }

    public remove(collection: string, objectId: string, result: (error: Error, data: any) => void): void {
        this.db.collection(collection).deleteOne({ _id: new ObjectID(objectId) }, (error, remove) => {
            return result(error, remove);
        });
    }
}
export default GenericClient;

----- generic-DAO.ts -----

import { inject, injectable } from 'inversify';
import GenericClient from './generic-client';
import IGenericEntity from '../interfaces';
@injectable()
export class GenericDAO<T extends IGenericEntity> {
    private _genericClient: GenericClient;
    private _model: string;

    constructor(
       genericClient: GenericClient,
       model: string
    ) {
        this._genericClient = genericClient;
        this._model = model;
    }

    public getUsers(): Promise<T[]> {
        return new Promise<T[]>((resolve, reject) => {
            this._genericClient.find(this._model, {}, (error, data: T[]) => {
                resolve(data);
            });
        });
    }

    public getUser(id: string): Promise<T> {
        return new Promise<T>((resolve, reject) => {
            this._genericClient.findOneById(this._model, id, (error, data: T) => {
                resolve(data);
            });
        });
    }

    public newUser(user: T): Promise<T> {
        return new Promise<T>((resolve, reject) => {
            this._genericClient.insert(this._model, user, (error, data: T) => {
                resolve(data);
            });
        });
    }

    public updateUser(id: string, user: T): Promise<T> {
        return new Promise<T>((resolve, reject) => {
            this._genericClient.update(this._model, id, user, (error, data: T) => {
                resolve(data);
            });
        });
    }

    public deleteUser(id: string): Promise<any> {
        return new Promise<any>((resolve, reject) => {
            this._genericClient.remove(this._model, id, (error, data: any) => {
                resolve(data);
            });
        });
    }
}

export default GenericDAO;

----- user-DAO.ts ------

import { inject, injectable } from 'inversify';
import GenericClient from './generic-client';
import TYPES from '../constants/types';
import IGenericEntity from '../interfaces';
import GenericDAO from './generic-DAO';
@injectable()
export class UserDAO<User extends IGenericEntity> extends GenericDAO<User> {
    private _dbClient: GenericClient;
    private _userModel: string;

    constructor(
        @inject(TYPES.GenericClient) genericClient: GenericClient,
        model: string
    ) {
        super(genericClient, model);
        this._dbClient = genericClient;
        this._userModel = model;
    }
}

export default UserDAO;

The problem is when I have in app.ts
container.bind>(TYPES.UserDAO).to( GenericDAO);

Any ideas? How can I apply the above idea as per @rmblstrp suggestion?

@Deviad - please create a separate issue.

This feature is now implemented and will be released in [email protected] :tada:

With the way that this has been fixed, it has broken another use case in a very strange way. I've logged https://github.com/inversify/InversifyJS/issues/854 to track the new behavior.

Was this page helpful?
0 / 5 - 0 ratings