这是我与猫鼬一起使用的解决方案
-
TenantsService 用于管理应用中的所有租户
@Injectable()
export class TenantsService {
constructor(
@InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
) {}
/**
* Save tenant data
*
* @param {CreateTenantDto} createTenant
* @returns {Promise<ITenant>}
* @memberof TenantsService
*/
async create(createTenant: CreateTenantDto): Promise<ITenant> {
try {
const dataToPersist = new this.tenantModel(createTenant);
// Persist the data
return await dataToPersist.save();
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/**
* Find details of a tenant by name
*
* @param {string} name
* @returns {Promise<ITenant>}
* @memberof TenantsService
*/
async findByName(name: string): Promise<ITenant> {
return await this.tenantModel.findOne({ name });
}
}
-
TenantAwareMiddleware 中间件从请求上下文中获取 tenant id。您可以在此处创建自己的逻辑以从请求标头或请求 url 子域中提取 tenant id。请求头提取方法如下所示。
如果您想提取子域,也可以通过调用req.subdomains 从Request 对象中提取子域来完成,这将为您提供子域列表,然后您可以从中获取您正在寻找的子域那个。
@Injectable()
export class TenantAwareMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Extract from the request object
const { subdomains, headers } = req;
// Get the tenant id from header
const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];
if (!tenantId) {
throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
}
// Set the tenant id in the header
req['tenantId'] = tenantId.toString();
next();
}
}
-
TenantConnection 此类用于使用 tenant id 创建新连接,如果现有连接可用,它将返回相同的连接(以避免创建额外的连接)。
@Injectable()
export class TenantConnection {
private _tenantId: string;
constructor(
private tenantService: TenantsService,
private configService: ConfigService,
) {}
/**
* Set the context of the tenant
*
* @memberof TenantConnection
*/
set tenantId(tenantId: string) {
this._tenantId = tenantId;
}
/**
* Get the connection details
*
* @param {ITenant} tenant
* @returns
* @memberof TenantConnection
*/
async getConnection(): Connection {
// Get the tenant details from the database
const tenant = await this.tenantService.findByName(this._tenantId);
// Validation check if tenant exist
if (!tenant) {
throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
}
// Get the underlying mongoose connections
const connections: Connection[] = mongoose.connections;
// Find existing connection
const foundConn = connections.find((con: Connection) => {
return con.name === `tenantDB_${tenant.name}`;
});
// Check if connection exist and is ready to execute
if (foundConn && foundConn.readyState === 1) {
return foundConn;
}
// Create a new connection
return await this.createConnection(tenant);
}
/**
* Create new connection
*
* @private
* @param {ITenant} tenant
* @returns {Connection}
* @memberof TenantConnection
*/
private async createConnection(tenant: ITenant): Promise<Connection> {
// Create or Return a mongo connection
return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
}
}
-
TenantConnectionFactory 这是自定义提供程序,可为您提供 tenant id 并帮助创建连接
// Tenant creation factory
export const TenantConnectionFactory = [
{
provide: 'TENANT_CONTEXT',
scope: Scope.REQUEST,
inject: [REQUEST],
useFactory: (req: Request): ITenantContext => {
const { tenantId } = req as any;
return new TenantContext(tenantId);
},
},
{
provide: 'TENANT_CONNECTION',
useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose> => {
// Set tenant context
connection.tenantId = context.tenantId;
// Return the connection
return connection.getConnection();
},
inject: ['TENANT_CONTEXT', TenantConnection],
},
];
-
TenantsModule - 在这里您可以看到作为提供程序添加的TenantConnectionFactory,并且正在导出以在其他模块中使用。
@Module({
imports: [
CoreModule,
],
controllers: [TenantsController],
providers: [
TenantsService,
TenantConnection,
...TenantConnectionFactory,
],
exports: [
...TenantConnectionFactory,
],
})
export class TenantsModule {}
-
TenantModelProviders - 由于您的租户模型依赖于租户连接,因此您的模型必须通过提供程序定义,然后包含在您初始化它们的模块中。
export const TenantModelProviders = [
{
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['TENANT_CONNECTION'],
},
];
-
UsersModule - 此类将使用模型。您还可以看到此处配置的中间件以作用于您的 Tenand db 路由。这种情况下,所有user 路由都是租户的一部分,将由租户数据库提供服务。
@Module({
imports: [
CoreModule,
TenantsModule,
],
providers: [
UsersService,
...TenantModelProviders,
],
controllers: [UsersController],
})
export class UsersModule implements NestModule {
configure(context: MiddlewareConsumer) {
context.apply(TenantAwareMiddleware).forRoutes('/users');
}
}
-
UsersService - 从用户模块访问租户数据库的示例实现
@Injectable()
export class UsersService {
constructor(
@Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
@Inject('USER_MODEL') private userModel: Model<IUser>,
) {
Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
}
/**
* Create a new user
*
* @param {CreateUserDto} user
* @returns {Promise<IUser>}
* @memberof UsersService
*/
async create(user: CreateUserDto): Promise<IUser> {
try {
const dataToPersist = new this.userModel(user);
// Persist the data
return await dataToPersist.save();
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/**
* Get the list of all users
*
* @returns {Promise<IUser>}
* @memberof UsersService
*/
async findAll(): Promise<IUser> {
return await this.userModel.find({});
}
}