如何在NESTJS中设置多租户 [英] How can i setup multitenant in NESTJS

查看:192
本文介绍了如何在NESTJS中设置多租户的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想连接到基于子域(多租户)的任何数据库,但是我不确定该怎么做.

I want to connect to any database based on the subdomain (multi-tenant), but i'm not sure how can i do it.

我的代码在启动应用程序时运行,但是我不知道如何基于子域更改数据源.

My code runs when the app is started, but i don't know how to change the Datasource based on subdomain.

PS:我为每个请求创建了中间件,但是我不知道如何更改源.

PS: I created middleware on each request, but I don't know how to change the source.

我的数据库有以下代码:

I have the following code for my DB:

import { connect, createConnection } from 'mongoose';
import { SERVER_CONFIG, DB_CONNECTION_TOKEN } from '../server.constants';

 const opts = {
    useCreateIndex: true,
    useNewUrlParser: true,
    keepAlive: true,
    socketTimeoutMS: 30000,
    poolSize: 100,
    reconnectTries: Number.MAX_VALUE,
    reconnectInterval: 500,
    autoReconnect: true,
  };
export const databaseProviders = [
  {
    provide: DB_CONNECTION_TOKEN,
    useFactory: async () => {
      try {
        console.log(`Connecting to ${ SERVER_CONFIG.db }`);
        return await createConnection(`${SERVER_CONFIG.db}`, opts);
      } catch (ex) {
        console.log(ex);
      }

    },
  }
];

我想基于子域(多租户)更改每个请求中的数据源

I want to change my datasource in each request based on subdomain (multi-tenant)

推荐答案

这是我与猫鼬一起使用的解决方案

Here is a solution that i used with mongoose

  1. TenantsService用于管理应用程序中的所有租户
  1. TenantsService used to manage all tenants in the application

@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 });
    }
}

  1. TenantAwareMiddleware中间件,用于从请求上下文中获取tenant id.您可以在此处做出自己的逻辑,以从请求标头或请求url子域中提取tenant id.请求标头提取方法如下所示.
  1. TenantAwareMiddleware middleware to get the tenant id from the request context. You can make your own logic here to extract the tenant id, either from request header or from request url subdomain. Request header extraction method is shown here.

如果要提取子域,则可以通过调用req.subdomainsRequest对象中提取子域来完成,这将为您提供子域列表,然后您可以从中找到所需的子域.那个.

If you want to extract the subdomain the same can be done by extracting it from the Request object by calling req.subdomains, which would give you a list of subdomains and then you can get the one you are looking for from that.

@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();
    }
}

  1. TenantConnection此类用于使用tenant id创建新连接,并且如果有可用的现有连接,它将返回相同的连接(以避免创建其他连接).
  1. TenantConnection this class is used to create new connection using tenant id and if there is an existing connection available it would return back the same connection (to avoid creating additional connections).

@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'));
    }
}

  1. TenantConnectionFactory这是自定义提供程序,可为您提供tenant id并有助于创建连接
  1. TenantConnectionFactory this is custom provider which gets you the tenant id and also helps in creation of the connection

// 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],
    },
];

  1. TenantsModule-在这里您可以看到TenantConnectionFactory作为提供者添加并正在导出以便在其他模块中使用.
  1. TenantsModule - Here you can see the TenantConnectionFactory added as a provider and is being exported to be used inside other modules.

@Module({
  imports: [
    CoreModule,
  ],
  controllers: [TenantsController],
  providers: [
    TenantsService,
    TenantConnection,
    ...TenantConnectionFactory,
  ],
  exports: [
    ...TenantConnectionFactory,
  ],
})
export class TenantsModule {}

  1. TenantModelProviders-由于租户模型取决于租户连接,因此必须通过提供程序定义模型,然后将其包含在初始化它们的模块中.
  1. TenantModelProviders - Since your tenant models depends on the tenant connection, your models have to defined through a provider and then included inside the module where you initialise them.

export const TenantModelProviders = [
    {
        provide: 'USER_MODEL',
        useFactory: (connection: Connection) => connection.model('User', UserSchema),
        inject: ['TENANT_CONNECTION'],
    },
];

  1. UsersModule-此类将使用模型.您还可以看到此处配置的中间件可以作用于Tenand数据库路由.在这种情况下,所有user路由都是租户的一部分,并将由租户db服务.
  1. UsersModule - This class will be using the models. You can also see the middleware being configured here to act upon your tenand db routes. This case all the user routes are part of the tenant and will be served by tenant db.

@Module({
  imports: [
    CoreModule,
    TenantsModule,
  ],
  providers: [
    UsersService,
    ...TenantModelProviders,
  ],
  controllers: [UsersController],
})
export class UsersModule implements NestModule {
  configure(context: MiddlewareConsumer) {
    context.apply(TenantAwareMiddleware).forRoutes('/users');
  }
}

  1. UsersService-从用户模块访问租户db的示例实现
  1. UsersService - Example implementation of accessing tenant db from user module

@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({});
    }
}

这篇关于如何在NESTJS中设置多租户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆