我如何在NESTJS中设置多租户

时间:2019-05-10 22:49:32

标签: mongodb multi-tenant nestjs

我想连接到基于子域(多租户)的任何数据库,但是我不确定该怎么做,因为我的数据库具有以下代码。

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

PD:我在每个请求中都创建了中间件,但是我不知道如何更改源。

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

    },
  }
];

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

2 个答案:

答案 0 :(得分:12)

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

  1. 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 });
    }
}

  1. TenantAwareMiddleware中间件,以从请求上下文中获取tenant id。您可以在此处做出自己的逻辑,以从请求标头或请求url子域中提取tenant id。请求标头提取方法如下所示。

如果要提取子域,则可以通过调用Requestreq.subdomains对象中提取子域来完成,这将为您提供子域列表,然后您就可以得到一个子域。正在从中寻找。

@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创建新连接,如果有可用的现有连接,它将返回相同的连接(以避免创建其他连接)。
@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,并有助于创建连接
// 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作为提供者被添加,并且正在导出以便在其他模块中使用。
@Module({
  imports: [
    CoreModule,
  ],
  controllers: [TenantsController],
  providers: [
    TenantsService,
    TenantConnection,
    ...TenantConnectionFactory,
  ],
  exports: [
    ...TenantConnectionFactory,
  ],
})
export class TenantsModule {}
  1. TenantModelProviders-由于租户模型取决于租户连接,因此必须通过提供程序定义模型,然后将其包含在初始化它们的模块中。
export const TenantModelProviders = [
    {
        provide: 'USER_MODEL',
        useFactory: (connection: Connection) => connection.model('User', UserSchema),
        inject: ['TENANT_CONNECTION'],
    },
];
  1. UsersModule-此类将使用模型。您还可以看到此处配置的中间件可以作用于Tenand数据库路由。在这种情况下,所有user路由都是租户的一部分,并将由租户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-从用户模块访问租户数据库的示例实现
@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({});
    }
}

答案 1 :(得分:0)

我们还为NestJS设置提供了多用户设置。
您可能拥有一个中间件,该中间件根据请求来决定使用哪个数据源。在我们的示例中,我们使用TypeORM,它在NestJS中具有很好的集成。 TypeORM软件包中包含一些有用的功能。

中间件

export class AppModule {
  constructor(private readonly connection: Connection) {
  }

  configure(consumer: MiddlewareConsumer): void {
    consumer
      .apply(async (req, res, next) => {
        try {
          getConnection(tenant);
          next();
        } catch (e) {
          const tenantRepository = this.connection.getRepository(tenant);
          const tenant = await tenantRepository.findOne({ name: tenant });
          if (tenant) {
            const createdConnection: Connection = await createConnection(options);
            if (createdConnection) {
              next();
            } else {
              throw new CustomNotFoundException(
                'Database Connection Error',
                'There is a Error with the Database!',
              );
            }
          }
        }
      }).forRoutes('*');
   }

这是我们中间件的一个示例。 TypeORM在内部管理连接。因此,您要尝试的第一件事是为该特定租户加载连接。如果有一个,那么好的就创建一个。这里的好处是,一旦创建了连接,TypeORM连接管理器中就可以使用它。这样一来,您在路线中就始终保持连接。
在您的路线中,您需要为住户提供身份证明。在我们的情况下,这只是从URL中提取的字符串。无论它是什么值,您都可以将其绑定到中间件内部的请求对象。在您的控制器中,您再次提取该值并将其传递给您的服务。然后,您必须为租户和您的物品加载存储库。

服务等级

@Injectable()
export class SampleService {

  constructor() {}

  async getTenantRepository(tenant: string): Promise<Repository<Entity>> {
    try {
      const connection: Connection = await getConnection(tenant);
      return connection.getRepository(Property);
    } catch (e) {
      throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
    }
  }

  async findOne(params: Dto, tenant: string) {

    const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);

    return await propertyRepository.findOne({ where: params });

  }

这就是服务在我们的应用程序中的样子。

希望这会激发您的灵感,并帮助您解决问题:)