I am supporting multiple databases without using ORM tools. For this example I will show an example for Postgres and MSSQL UserQueries
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.
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:
IUserQueryFactory
returns IUserQueries
based on input type or returns default one (one defined in configuration) if type is not providerIUserQueries
is basically implemented through IUserQueryFactory
which means that whole source of creating IUserQueries
is at single place and easily maintanable.