Search code examples
javascriptnode.jstypescriptdependency-injectioninversifyjs

pass type to dependency injection container


I am supporting multiple databases without using ORM tools. For this example I will show an example for Postgres and MSSQL UserQueries

enter image description here

I have an interface IDataBase with multiple databases implementing this interface.

@injectable()
export class MssqlDatabase implements IDatabase {
    public async connect(): Promise<void> {
    }

    public async disconnect(): Promise<void> {
    }
}

Further I have my IUserQueries interface defining the queries

export interface IUserQueries {
    fetchUsers(): Promise<QueryResult>;
}

QueryResult is a custom class to make sure, every database query returns the same data object for my API. My API calls the specific query from the target interface. If mssql is the identifier in the config file, my application should create a connection to the MSSQL database. But my API files shouldn't care and don't know this (obviously, that's what a interface does).

A basic example would be

@injectable()
export class UserRepository implements IUserRepository {
    public userQueries: IUserQueries;

    constructor(@inject(IoCTypes.IUserQueries) userQueries: IUserQueries) {
        this.userQueries = userQueries;
    }
}

Now things get tricky here. My DI container:

const container: Container = new Container();
container.bind<IUserQueries>(IoCTypes.IUserQueries).to(/* what? */);
export { container };

The DI container doesn't know which query file to pick. It could be the MSSQLUserQueries or PostgresUserQueries. How can I solve this problem? It is possible to select the right queries when starting the server (app.ts) but as far as I understood, this DI container is completely independent from the application and just acts as a config file so I shouldn't pass in an abstract UserQueries class.


Solution

  • You could make use of factory pattern:

    interface IUserQueryFactory {
        get(): IUserQueries;
        get(type: string): IUserQueries;
    }
    
    class UserQueryFactory implements IUserQueryFactory {
        get(): IUserQueries {
            const defaultValue = config.database; // or whatever returns your "mssql" from config
            return this.get(defaultValue);
        }
    
        get(type: string): IUserQueries {
            switch (type) {
                case "mssql":
                    return new MSSQLUserQueries();
                case "postgresql":
                    return new PostgresUserQueries();
                default:
                    return null;
            }
        }
    }
    
    const container: Container = new Container();
    container.bind<IUserQueryFactory>(IoCTypes.IUserQueryFactory).to(UserQueryFactory);
    container.bind<IUserQueries>(IoCTypes.IUserQueries).toDynamicValue((context: interfaces.Context) => { return context.container.get<IUserQueryFactory>(IoCTypes.IUserQueryFactory).get(); });
    export { container };
    

    I'm not sure I've got the whole syntax correctly since this is not tested, but I guess you get the idea:

    1. IUserQueryFactory returns IUserQueries based on input type or returns default one (one defined in configuration) if type is not provider
    2. Default implementation for IUserQueries is basically implemented through IUserQueryFactory which means that whole source of creating IUserQueries is at single place and easily maintanable.