How can i setup multitenant in NESTJS

后端 未结 2 739
爱一瞬间的悲伤
爱一瞬间的悲伤 2021-01-13 13:08

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

2条回答
  •  旧巷少年郎
    2021-01-13 13:21

    Here is a solution that i used with mongoose

    1. TenantsService used to manage all tenants in the application
    @Injectable()
    export class TenantsService {
        constructor(
            @InjectModel('Tenant') private readonly tenantModel: Model,
        ) {}
    
        /**
         * Save tenant data
         *
         * @param {CreateTenantDto} createTenant
         * @returns {Promise}
         * @memberof TenantsService
         */
        async create(createTenant: CreateTenantDto): Promise {
            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}
         * @memberof TenantsService
         */
        async findByName(name: string): Promise {
            return await this.tenantModel.findOne({ name });
        }
    }
    
    
    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.

    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 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 {
            // Create or Return a mongo connection
            return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
        }
    }
    
    
    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  => {
                // Set tenant context
                connection.tenantId = context.tenantId;
    
                // Return the connection
                return connection.getConnection();
            },
            inject: ['TENANT_CONTEXT', TenantConnection],
        },
    ];
    
    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 - 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 - 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 - 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,
        ) {
            Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
        }
    
        /**
         * Create a new user
         *
         * @param {CreateUserDto} user
         * @returns {Promise}
         * @memberof UsersService
         */
        async create(user: CreateUserDto): Promise {
            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}
         * @memberof UsersService
         */
        async findAll(): Promise {
            return await this.userModel.find({});
        }
    }
    
    

提交回复
热议问题