Skip to content

Instantly share code, notes, and snippets.

@adriano-di-giovanni
Last active December 24, 2023 23:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save adriano-di-giovanni/8c3e1069c5a14d947773c64c41501bd6 to your computer and use it in GitHub Desktop.
Save adriano-di-giovanni/8c3e1069c5a14d947773c64c41501bd6 to your computer and use it in GitHub Desktop.
Multi-tenant Mongoose module for Nest
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TenancyModule } from '@app/tenancy';
import { OrderModule } from './order';
import { PaymentModule } from './payment';
import { Request } from 'express';
@Module({
imports: [TenancyModule.forRoot({
tenantId: (req: Request) => req.get('X-TenantId'),
options: () => ({}),
uri: (tenantId: string) => `mongodb://localhost/tenant-${tenantId}`,
}), OrderModule, PaymentModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
import { getTenantModelToken, getTenantModelDefinitionToken } from '../utils';
import {
ModelDefinition,
ModelDefinitionMap,
ConnectionMap,
} from '../types';
import { Provider, Scope } from '@nestjs/common';
import { Token } from '../enums';
import * as mongoose from 'mongoose';
export const createProviders = (definitions: ModelDefinition[]): Provider[] => {
const providers: Provider[] = [];
definitions.forEach(definition => {
const { name, schema, collection } = definition;
providers.push({
inject: [
Token.MODEL_DEFINITION_MAP,
Token.CONNECTION_MAP,
],
provide: getTenantModelDefinitionToken(name),
useFactory: (
modelDefinitionMap: ModelDefinitionMap,
connectionMap: ConnectionMap,
) => {
const exists = modelDefinitionMap.has(name);
if (!exists) {
modelDefinitionMap.set(name, { ...definition });
connectionMap.forEach(connection => {
connection.model(name, schema, collection);
});
}
},
});
providers.push({
inject: [Token.TENANT_CONNECTION],
provide: getTenantModelToken(name),
scope: Scope.REQUEST,
useFactory: (tenantConnection: mongoose.Connection) =>
tenantConnection.model(name),
});
});
return providers;
};
import { Module } from '@nestjs/common';
import { OrderService } from './order.service';
import { OrderController } from './order.controller';
import { TenancyModule } from '@app/tenancy';
import { OrderSchema } from './schemas';
@Module({
imports: [TenancyModule.forFeature([{ name: 'Order', schema: OrderSchema }])],
providers: [OrderService],
controllers: [OrderController],
})
export class OrderModule {}
import { Injectable } from '@nestjs/common';
import { InjectTenantModel } from '@app/tenancy';
import { Model } from 'mongoose';
import { Order } from './interfaces';
import { CreateOrderDto } from './dtos';
@Injectable()
export class OrderService {
constructor(
@InjectTenantModel('Order') private readonly orderModel: Model<Order>,
) {}
async create(dto: CreateOrderDto): Promise<Order> {
return new this.orderModel(dto).save()
}
async findAll(): Promise<Order[]> {
return this.orderModel.find().exec()
}
}
import { createProviders } from './factories';
import { Global, Module, DynamicModule } from '@nestjs/common';
import {
ModelDefinition,
} from './types';
@Global()
@Module({})
export class TenancyFeatureModule {
static register(models: ModelDefinition[]): DynamicModule {
const providers = createProviders(models);
return {
module: TenancyFeatureModule,
providers,
exports: providers,
};
}
}
import {
Global,
Module,
DynamicModule,
Scope,
OnApplicationShutdown,
} from '@nestjs/common';
import { REQUEST, ModuleRef } from '@nestjs/core';
import { Request } from 'express';
import {
TenancyModuleOptions,
ConnectionMap,
ModelDefinitionMap,
} from './types';
import { Token } from './enums';
import * as mongoose from 'mongoose';
@Global()
@Module({})
export class TenancyRootModule implements OnApplicationShutdown {
constructor(private readonly moduleRef: ModuleRef) {}
async onApplicationShutdown() {
const connectionMap: ConnectionMap = this.moduleRef.get(
Token.CONNECTION_MAP,
);
await Promise.all(
[...connectionMap.values()].map(connection => connection.close()),
);
}
static register(options: TenancyModuleOptions): DynamicModule {
const tenancyModuleOptions = {
provide: Token.TENANCY_MODULE_OPTIONS,
useValue: { ...options },
};
const connectionMap = {
provide: Token.CONNECTION_MAP,
useFactory: (): ConnectionMap => new Map(),
};
const modelDefinitionMap = {
provide: Token.MODEL_DEFINITION_MAP,
useFactory: (): ModelDefinitionMap => new Map(),
};
const tenantConnection = {
inject: [
REQUEST,
Token.TENANCY_MODULE_OPTIONS,
Token.CONNECTION_MAP,
Token.MODEL_DEFINITION_MAP,
],
provide: Token.TENANT_CONNECTION,
scope: Scope.REQUEST,
useFactory: async (
req: Request,
options: TenancyModuleOptions,
connectionMap: ConnectionMap,
modelDefinitionMap: ModelDefinitionMap,
): Promise<mongoose.Connection> => {
const tenantId = options.tenantId(req);
const exists = connectionMap.has(tenantId);
if (exists) {
return connectionMap.get(tenantId);
}
const connection = mongoose.createConnection(options.uri(tenantId), {
useNewUrlParser: true,
useUnifiedTopology: true,
...options.options(tenantId),
});
modelDefinitionMap.forEach(definition => {
const { name, schema, collection } = definition;
connection.model(name, schema, collection);
});
connectionMap.set(tenantId, connection);
return connection;
},
};
const providers = [
modelDefinitionMap,
tenancyModuleOptions,
tenantConnection,
connectionMap,
];
return {
module: TenancyRootModule,
providers,
exports: providers,
};
}
}
import {
ModelDefinition,
TenancyModuleOptions,
} from './types';
import { Module, DynamicModule } from '@nestjs/common';
import { TenancyRootModule } from './tenancy-root.module';
import { TenancyFeatureModule } from './tenancy-feature.module';
@Module({})
export class TenancyModule {
static forRoot(options: TenancyModuleOptions): DynamicModule {
return {
module: TenancyModule,
imports: [TenancyRootModule.register(options)],
};
}
static forFeature(models: ModelDefinition[]): DynamicModule {
return {
module: TenancyModule,
imports: [TenancyFeatureModule.register(models)],
};
}
}
@Robokishan
Copy link

where is InjectTenantModel coming from ?

@adriano-di-giovanni
Copy link
Author

@Robokishan I can't retrieve the code. As far as I remember, it was a convenience method using @Inject().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment