I am working on a multitenant app and in that, I use mongoose & typegoose in combination to switch the DB for specific tenants.
Below is a minimal reproducible code that can be seen by installing typegoose and mongoose packages.
import { getModelForClass, mongoose, prop } from '@typegoose/typegoose';
import { AnyParamConstructor, ReturnModelType } from '@typegoose/typegoose/lib/types';
export class DB_ROLES {
@prop({ type: String })
name: string;
}
const ROLES_MODEL = getModelForClass(DB_ROLES, {
schemaOptions: {
timestamps: true,
collection: 'roles',
},
});
export class DB_USERS {
@prop({ type: String })
name: string;
}
const DB_USERS_MODEL = getModelForClass(DB_USERS, {
schemaOptions: {
collection: 'users',
timestamps: true,
},
});
//DATABASE UTILITY
class DatabaseUtil {
public database: mongoose.Connection;
public connectDatabase = async (): Promise<boolean> => {
return new Promise((resolve, reject) => {
const uri = environment_variables.MONGODB_CONNECTION_STRING ?? '';
if (this.database) {
return;
}
mongoose.connect(uri, {});
this.database = mongoose.connection;
this.database.once('open', async () => {
console.log('Connected to database');
resolve(true);
});
this.database.on('error', () => {
console.log('Error connecting to database');
reject(false);
});
});
};
getModelForDb<T extends AnyParamConstructor<any>>(databaseName: string, model: ReturnModelType<T>): ReturnModelType<T> & T {
const db = Mongoose.connection.useDb(databaseName);
const DbModel = db.model(model.modelName, model.schema) as ReturnModelType<T> & T;
return DbModel;
}
getModelsForDbWithKey<P extends string, T extends AnyParamConstructor<any>, K extends { key: P; model: ReturnModelType<T> }>(
databaseName: string,
models: K[]
): Partial<Record<P, ReturnModelType<T> & T>> {
const db = mongoose.connection.useDb(databaseName);
let result: Partial<Record<P, ReturnModelType<T> & T>> = {};
let allModels: (ReturnModelType<T> & T)[] = [];
models.forEach((value) => {
result[value.key] = db.model(value.model.modelName, value.model.schema) as ReturnModelType<T> & T;
});
return result;
}
}
const DBUtil = new DatabaseUtil();
let singleModel = DBUtil.getModelForDb('base_db', ROLES_MODEL);
singleModel.find({
//INTELLISENSE WORKS HERE
});
let requiredModels = DBUtil.getModelsForDbWithKey('base_db', [
{ key: 'roles', model: ROLES_MODEL },
{ key: 'users', model: DB_USERS_MODEL },
]);
let roleModel = requiredModels['roles']?.find({
//INTELLISENSE DOESN'T WORK
});
When I use single Model then I get intellisense as well
Now I am not able to get the typing for the model that I have passed.
This is what I get in return when I hover over the requiredModels object. So is there any way I can get proper typings with Generics. Models can be of different schema so that have different return types.
For your case, you are probably searching for something like a type that can map from a array to a object without losing the types (Like i had asked here after reading this question).
The resulting code would be:
// your models before here
// Extracted the type from the constraint to a interface
interface ModelListEntry {
key: string;
model: ReturnModelType<any>;
}
// Mapper type that maps a input array of "ModelListEntry" to a Record where the key is from "key" and the value is "model"
// the key-value information is only kepts if the input array is readonly (const)
type ModelMapListEntryArrayToRecord<A extends readonly ModelListEntry[]> = {
// see https://stackoverflow.com/a/73141433/8944059
[E in Extract<keyof A, `${number}`> as A[E]['key']]: A[E]['model'];
};
// DATABASE UTILITY
class DatabaseUtil {
public database!: mongoose.Connection;
public connectDatabase = async (): Promise<boolean> => {
return new Promise((resolve, reject) => {
const uri = 'mongodb://localhost:27017/';
if (this.database) {
return;
}
mongoose.connect(uri, {});
this.database = mongoose.connection;
this.database.once('open', async () => {
console.log('Connected to database');
resolve(true);
});
this.database.on('error', () => {
console.log('Error connecting to database');
reject(false);
});
});
};
getModelForDb<T extends AnyParamConstructor<any>>(databaseName: string, model: ReturnModelType<T>): ReturnModelType<T> & T {
const db = mongoose.connection.useDb(databaseName);
const DbModel = db.model(model.modelName, model.schema) as ReturnModelType<T> & T;
return DbModel;
}
getModelsForDbWithKey<List extends readonly ModelListEntry[]>(databaseName: string, models: List): ModelMapListEntryArrayToRecord<List> {
const db = mongoose.connection.useDb(databaseName);
const result: Record<string, ModelListEntry['model']> = {};
for (const { key, model } of models) {
result[key] = db.model(model.modelName, model.schema);
}
return result as any;
}
}
const DBUtil = new DatabaseUtil();
const singleModel = DBUtil.getModelForDb('base_db', ROLES_MODEL);
singleModel.find({
// INTELLISENSE WORKS HERE
});
const requiredModels = DBUtil.getModelsForDbWithKey('base_db', [
{ key: 'roles', model: ROLES_MODEL },
{ key: 'users', model: DB_USERS_MODEL },
] as const); // change the input array to be read-only, otherwise typescript will just "forget" about the explicit values
requiredModels.roles; // correct type "ReturnModelType<typeof DB_ROLES>"
requiredModels.users; // correct type "ReturnModelType<typeof DB_USERS>"
requiredModels.none; // error
const roleModel = requiredModels['roles']?.find({
// INTELLISENSE WORKS NOW
});
PS: this answer is thanks to the answer from the mentioned question and its comments.