I'd like to use dynamic collection names based on current year.
For example: From 'products' to 'products2020'.
Using NESTJS, I have to import "module.forFeature" with an specifyc collection name.
import { Module } from '@nestjs/mon'
import { MongooseModule } from '@nestjs/mongoose'
@Module({
imports: [
MongooseModule.forFeature([
{
name: 'Products',
schema: ProductsSchema
}
])
],
controllers: [ProductsController],
providers: [ProductsService]
})
And the same happens with injection at service:
import { Injectable } from '@nestjs/mon'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
@Injectable()
export class ProductsService {
constructor(
@InjectModel('Products')
private readonly productsModel: Model<Products>
) {}
}
And finally, here's my schema:
import { Schema } from 'mongoose'
export const ProductsSchema = new Schema(
{
_id: { Type: String, required: true },
code: String
},
{
collection: 'Products'
}
)
Is there some way to achieve dynamic naming?
Thanks a lot !
I'd like to use dynamic collection names based on current year.
For example: From 'products' to 'products2020'.
Using NESTJS, I have to import "module.forFeature" with an specifyc collection name.
import { Module } from '@nestjs/mon'
import { MongooseModule } from '@nestjs/mongoose'
@Module({
imports: [
MongooseModule.forFeature([
{
name: 'Products',
schema: ProductsSchema
}
])
],
controllers: [ProductsController],
providers: [ProductsService]
})
And the same happens with injection at service:
import { Injectable } from '@nestjs/mon'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
@Injectable()
export class ProductsService {
constructor(
@InjectModel('Products')
private readonly productsModel: Model<Products>
) {}
}
And finally, here's my schema:
import { Schema } from 'mongoose'
export const ProductsSchema = new Schema(
{
_id: { Type: String, required: true },
code: String
},
{
collection: 'Products'
}
)
Is there some way to achieve dynamic naming?
Thanks a lot !
Share Improve this question asked Jul 21, 2020 at 15:02 PaulLeonHPaulLeonH 631 silver badge4 bronze badges 2-
Why can't you just keep
year
property on documents in theProducts
collection? – Älskar Commented Jul 21, 2020 at 15:29 - @ethane Beacuse it's a requirement to handle it with that dynamic naming. – PaulLeonH Commented Jul 21, 2020 at 15:53
4 Answers
Reset to default 2I stumble into a similar issue, and the way I resolved was using the MongooseModule.forFeatureAsync
method. The model and schema declaration are the same as in the nestjs docs.
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: UsersModel.name,
imports: [EnvironmentModule],
inject: [EnvironmentService],
useFactory: (envService: EnvironmentService) => {
const env = envService.getEnv();
const schema = UsersSchema.set(
'collection',
`${env.countryCode}-users`,
);
return schema;
},
},
]),
...
],
providers: []
...
I was also searching for this, and after collecting some answers and websites, I finally figured out how to do dynamic naming of the collections.
Since I want all configured for also testing, I don't do a direct configuration of Mongoose in the AppModule import, but I use the nestjs config module. Still, I don't see why it shouldn't work for the direct configuration. In this example I am using two databases, one for the classic models (default), one for the dynamic (data). Let's start. You can find all the files here. In detail, what is important is the reports.service.ts
that I will show below
import { HttpException, HttpStatus, Injectable } from '@nestjs/mon';
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
import { DeleteResult } from 'mongodb';
import { Connection, Model, Types } from 'mongoose';
import { Report } from './report.entity';
import ObjectId = Types.ObjectId;
@Injectable()
export class ReportsService {
constructor(
@InjectModel('Report', 'default') protected readonly model: Model<Report>,
@InjectConnection('data') private dataConnection: Connection,
) {}
getAll(): Promise<Report[]> {
return this.model.aggregate().match({}).collation({ locale: 'en' }).exec();
}
find(id: string): Promise<Report> {
return this.model
.aggregate()
.match({ _id: new ObjectId(id) })
.collation({ locale: 'en' })
.exec()
.then((result) => {
if (result?.length) return result[0];
throw new HttpException('NOT FOUND', HttpStatus.NOT_FOUND);
});
}
async update(id: string, objParams: Report): Promise<Report> {
return this.model.findByIdAndUpdate(id, objParams);
}
async create(obj: Report): Promise<Report> {
return this.model.create(obj);
}
async delete(obj: Report): Promise<DeleteResult> {
return this.model.deleteOne({ _id: obj['_id'] });
}
// ------------------------------------------------
// Here starts the dynamic collection part
// ------------------------------------------------
async saveData(reportId: string, data: any[], replace: boolean) {
const collection = this.dataConnection.collection(reportId);
if (replace) await collection.deleteMany({});
return collection.insertMany(data);
}
loadData(reportId: string, filter: any = {}) {
return this.dataConnection.collection(reportId).find(filter);
}
}
Breaking down the elements, in the constructor we have
@InjectConnection('data') private dataConnection: Connection
that inject the connection. InjectConnection takes in input the name of the connection if you specified one in the AppModule, otherwise you can leave it blank.
Then this.dataConnection.collection('customName')
makes the dynamic schema connection like what happens with the usual schema in the module with MongooseModule.forFeature
and then with the injection of the model in the service constructor. Where I wrote customName
, you can specify the name of the collection you want to use, a string, a variable, an environment variable, something you take from the database or from the controller request, or whatever source you want.
From that, we can use the usual Mongoose functions; in the latest two methods, we have an example of find, delete, and create many.
Feel free to ask questions if anything isn't clear enough.
I've looking for a solution to this kind of problem but i've hit a wall and there was no clear way to do it.
Below (minimal) code instantiate Services each bound to a specific model depending on a country parameter
. i.e ServiceX
bound to Model of Database X, ServiceY
bound to the same Model in Database Y
But here is what i managed to do. You can absolutely do a work around to fit your needs
First es the model/interface. Commonly used between different services
export interface User extends Document {
readonly username: string;
readonly password: string;
}
export const UserSchema = new mongoose.Schema(
{
_id: mongoose.ObjectId,
username: String,
password: String
},
{ collection: 'accounts', autoCreate: true }
);
Service definition is indeed the same for every model in different database/collection
@Injectable()
export class XUserService implements OnModuleInit{
constructor(
private userModel: Model<User>,
) {
}
////////////////////////////////////////////////////////////////////////////
async onModuleInit(): Promise<any> {
console.log(`inside service dbname=: ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
// await new this.userModel({_id: mongoose.Types.ObjectId(), username: 'test', password: 'test', flag: this.c}).save()
}
async insert(){
console.log(`inside service dbname=: ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
await new this.userModel({
_id: mongoose.Types.ObjectId(),
username: this.userModel.db.name,
password: '0000'
}).save();
}
async findOne(): Promise<User>{
console.log(`inside service in : ${this.userModel.db.name} > ${this.userModel.collection.collectionName}` );
return this.userModel.findOne()
}
}
For Module, i made a DynamicModule
- Import DBConnections
- Create a Model for each need, ( for my case, one model in each Database )
- Create and bind each
Model
to aService
, so the instantiation of the service will be correct
@Module({
})
export class XUserModule{
static register( /*use can pass parameter here*/): DynamicModule{
return{
module: XUserModule,
imports: [
DatabaseModule
],
controllers: [
XUserController
],
providers: [
// Create Models here, #1 and #2 in two different database
{
provide: 'dz-'+'UserModel',
useFactory: (connection: Connection)=> {
return connection.model('User', UserSchema )
},
inject: [ dbname.shooffood('dz')+'Connection' ]
},{
provide: 'ca-'+'UserModel',
useFactory: (connection: Connection)=> {
return connection.model('User', UserSchema )
},
inject: [ dbname.shooffood('ca')+'Connection' ]
},
// Create Providers/Services for each Model and Inject the Model to the Service by `TokenString`
{
provide: 'dz' + XUserService.name,
useFactory: (m: any)=> {
console.log(m);
return new XUserService(m);
},
inject: [ 'dz-'+'UserModel' ]
},{
provide: 'ca' + XUserService.name,
useFactory: (m: any)=> {
console.log(m);
return new XUserService(m);
},
inject: [ 'ca-'+'UserModel' ]
}
],
// Export your service with the same `provide` name for later usage.
exports: [
'dz' + XUserService.name,
'ca' + XUserService.name
]
}
}
}
Just FYI, database module looks like
Constants dbname
are connection names and uri
are the connection string.
const databaseProviders = [
{
provide: dbname.admin+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.admin),
},{
provide: dbname.system+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.system),
},{
provide: dbname.shooffood('dz')+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.dzfood),
},{
provide: dbname.shooffood('ca')+'Connection',
useFactory: (): Promise<typeof mongoose> => mongoose.createConnection(uri.cafood),
}
];
@Module({
providers: [
...databaseProviders
],
exports: [
dbname.admin+'Connection',
dbname.system+'Connection',
dbname.shooffood('dz')+'Connection',
dbname.shooffood('ca')+'Connection'
]
})
export class DatabaseModule {}
As for Controller, there is only one that handle each service via request param :country
. But first i had to list all possible Models and services to include in the Application.
@Controller(':country')
export class XUserController {
private byCountryServices = new Map();
constructor(
// Inject all required services by `tokenString`
@Inject('dz' + XUserService.name) private dzUserService: XUserService,
@Inject('ca' + XUserService.name) private caUserService: XUserService,
) {
// Add to `<key, value>` Map for easy by param access
this.byCountryServices.set('dz', this.dzUserService );
this.byCountryServices.set('ca', this.caUserService );
}
@Get('post')
async post(
@Param('country') c: string
): Promise<string>{
await this.byCountryServices.get(c).insert()
return 'inserted in ' + c;
}
@Get('get')
async get(
@Param('country') c: string
): Promise<string>{
console.log('param: ' + c)
return await this.byCountryServices.get(c).findOne()
}
}
Finally you import the module in AppModule with
XUserModule.register()
Using your connection you search for the client:
const client = this.productsModel.db.getClient();
const database = client.db("data base name");
const collection = database.collection("collection name");
const data = { test: "test" };
const insertOneResult = await collection.insertOne(data);