NestJS : database connection (TypeORM) by request (subdomain)

て烟熏妆下的殇ゞ 提交于 2020-06-22 09:06:21

问题


I'm trying to build a SAAS product over Nest/TypeORM and I need to configure/change database connection by subdomain.

customer1.domain.com => connect to customer1 database
customer2.domain.com => connect to customer2 database
x.domain.com => connect to x database

How can I do that ? With interceptors or request-context (or Zone.js) ?

I don't know how to start. Is someone already do that ?


WIP : what I am currently doing :

  1. add all connections settings into ormconfig file
  2. create Middleware on all routes to inject subdomain into res.locals (instance name) and create/warn typeorm connection

    import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';
    import { getConnection, createConnection } from "typeorm";
    
    @Injectable()
    export class DatabaseMiddleware implements NestMiddleware {
        resolve(): MiddlewareFunction {
          return async (req, res, next) => {
              const instance = req.headers.host.split('.')[0]
              res.locals.instance = instance
    
              try {
                  getConnection(instance)
              } catch (error) {
                  await createConnection(instance)
              }
    
              next();
          };
        }
    }
    
  3. in Controller : get instance name from @Response and pass it to my Service

    @Controller('/catalog/categories')
    export class CategoryController {
        constructor(private categoryService: CategoryService) {}
    
        @Get()
        async getList(@Query() query: SearchCategoryDto, @Response() response): Promise<Category[]> {
          return response.send(
            await this.categoryService.findAll(response.locals.instance, query)
          )
        }
    
  4. in Service : get TypeORM Manager for given instance and query database through Repository

    @Injectable()
    export class CategoryService {
      // constructor(
      //   @InjectRepository(Category) private readonly categoryRepository: Repository<Category>
      // ) {}
    
      async getRepository(instance: string): Promise<Repository<Category>> {
          return (await getManager(instance)).getRepository(Category)
      }
    
      async findAll(instance: string, dto: SearchCategoryDto): Promise<Category[]> {
        let queryBuilder = (await this.getRepository(instance)).createQueryBuilder('category')
    
        if (dto.name) {
            queryBuilder.andWhere("category.name like :name", { name: `%${dto.name}%` })
        }
    
        return await queryBuilder.getMany();
      }
    

It seems to work but I not sure about pretty much everything :

  • connections poole (how many can I create connections into my ConnectionManager ?)
  • pass subdomain into response.locals... bad practice ?
  • readability / comprehension / adding lot of additional code...
  • side effects : I'm afraid to share connections between several subdomains
  • side effects : performance

It's not a pleasure to deals with response.send() + Promise + await(s) + pass subdomain everywhere...

Is there a way to get subdomain directly into my Service ?

Is there a way to get correct subdomain Connection/Repository directly into my Service and Inject it into my Controller ?


回答1:


I came up with another solution.

I created a middleware to get the connection for a specific tenant:

import { createConnection, getConnection } from 'typeorm';
import { Tenancy } from '@src/tenancy/entity/tenancy.entity';

export function tenancyConnection(...modules: Array<{ new(...args: any[]): 
any; }>) {

  return async (req, res, next) => {

    const tenant = req.headers.host.split(process.env.DOMAIN)[0].slice(0, -1);

    // main database connection
    let con = ...

    // get db config that is stored in the main db
    const tenancyRepository = await con.getRepository(Tenancy);
    const db_config = await tenancyRepository.findOne({ subdomain: tenant });

    let connection;
    try {
       connection = await getConnection(db_config.name);
    } catch (e) {
      connection = await createConnection(db_config.config);
    }

    // stores connection to selected modules
    for (let module of modules) {
      Reflect.defineMetadata('__tenancyConnection__', connection, module);
    }

    next();
  };
}

I added it to the main.ts:

const app = await NestFactory.create(AppModule);
app.use(tenancyConnection(AppModule));

To access the connection you can extend any service by:

export class TenancyConnection {

  getConnection(): Connection {
    return Reflect.getMetadata('__tenancyConnection__', AppModule);
  }
}

It is still a draft, but with this solution you can add, delete and edit the connection for each tenant at runtime. I hope this helps you further.




回答2:


I got inspired by yoh's solution but I tweaked it a bit according to the new features in NestJS. The result is less code.

1) I created DatabaseMiddleware

import { Injectable, NestMiddleware, Inject } from '@nestjs/common';
import { getConnection, createConnection, ConnectionOptions } from "typeorm";

@Injectable()
export class DatabaseMiddleware implements NestMiddleware {

  public static COMPANY_NAME = 'company_name';

  async use(req: any, res: any, next: () => void) {
    const databaseName = req.headers[DatabaseMiddleware.COMPANY_NAME];

    const connection: ConnectionOptions = {
      type: "mysql",
      host: "localhost",
      port: 3307,
      username: "***",
      password: "***",
      database: databaseName,
      name: databaseName,
      entities: [
        "dist/**/*.entity{.ts,.js}",
        "src/**/*.entity{.ts,.js}"
      ],
      synchronize: false
    };

    try {
      getConnection(connection.name);
    } catch (error) {
      await createConnection(connection);
    }

    next();
  }

}

2) in main.ts use it for every routes

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(new DatabaseMiddleware().use);
  ...

3) In service retrieve connection

import { Injectable, Inject } from '@nestjs/common';
import { Repository, getManager } from 'typeorm';
import { MyEntity } from './my-entity.entity';
import { REQUEST } from '@nestjs/core';
import { DatabaseMiddleware } from '../connections';

@Injectable()
export class MyService {
  private repository: Repository<MyEntity>;

  constructor(@Inject(REQUEST) private readonly request) { 
    this.repository = getManager(this.request.headers[DatabaseMiddleware.COMPANY_NAME]).getRepository(MyEntity);
  }

  async findOne(): Promise<MyEntity> {
    return await this.repository
    ...
  }

}



回答3:


I write an implementation for this issue for nest-mongodb, please check it out it might help.

Similar question https://stackoverflow.com/a/57842819/7377682

import {
    Module,
    Inject,
    Global,
    DynamicModule,
    Provider,
    OnModuleDestroy,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { MongoClient, MongoClientOptions } from 'mongodb';
import {
    DEFAULT_MONGO_CLIENT_OPTIONS,
    MONGO_MODULE_OPTIONS,
    DEFAULT_MONGO_CONTAINER_NAME,
    MONGO_CONTAINER_NAME,
} from './mongo.constants';
import {
    MongoModuleAsyncOptions,
    MongoOptionsFactory,
    MongoModuleOptions,
} from './interfaces';
import { getClientToken, getContainerToken, getDbToken } from './mongo.util';
import * as hash from 'object-hash';

@Global()
@Module({})
export class MongoCoreModule implements OnModuleDestroy {
    constructor(
        @Inject(MONGO_CONTAINER_NAME) private readonly containerName: string,
        private readonly moduleRef: ModuleRef,
    ) {}

    static forRoot(
        uri: string,
        dbName: string,
        clientOptions: MongoClientOptions = DEFAULT_MONGO_CLIENT_OPTIONS,
        containerName: string = DEFAULT_MONGO_CONTAINER_NAME,
    ): DynamicModule {

        const containerNameProvider = {
            provide: MONGO_CONTAINER_NAME,
            useValue: containerName,
        };

        const connectionContainerProvider = {
            provide: getContainerToken(containerName),
            useFactory: () => new Map<any, MongoClient>(),
        };

        const clientProvider = {
            provide: getClientToken(containerName),
            useFactory: async (connections: Map<any, MongoClient>) => {
                const key = hash.sha1({
                    uri: uri,
                    clientOptions: clientOptions,
                });
                if (connections.has(key)) {
                    return connections.get(key);
                }
                const client = new MongoClient(uri, clientOptions);
                connections.set(key, client);
                return await client.connect();
            },
            inject: [getContainerToken(containerName)],
        };

        const dbProvider = {
            provide: getDbToken(containerName),
            useFactory: (client: MongoClient) => client.db(dbName),
            inject: [getClientToken(containerName)],
        };

        return {
            module: MongoCoreModule,
            providers: [
                containerNameProvider,
                connectionContainerProvider,
                clientProvider,
                dbProvider,
            ],
            exports: [clientProvider, dbProvider],
        };
    }

    static forRootAsync(options: MongoModuleAsyncOptions): DynamicModule {
        const mongoContainerName =
            options.containerName || DEFAULT_MONGO_CONTAINER_NAME;

        const containerNameProvider = {
            provide: MONGO_CONTAINER_NAME,
            useValue: mongoContainerName,
        };

        const connectionContainerProvider = {
            provide: getContainerToken(mongoContainerName),
            useFactory: () => new Map<any, MongoClient>(),
        };

        const clientProvider = {
            provide: getClientToken(mongoContainerName),
            useFactory: async (
                connections: Map<any, MongoClient>,
                mongoModuleOptions: MongoModuleOptions,
            ) => {
                const { uri, clientOptions } = mongoModuleOptions;
                const key = hash.sha1({
                    uri: uri,
                    clientOptions: clientOptions,
                });
                if (connections.has(key)) {
                    return connections.get(key);
                }
                const client = new MongoClient(
                    uri,
                    clientOptions || DEFAULT_MONGO_CLIENT_OPTIONS,
                );
                connections.set(key, client);
                return await client.connect();
            },
            inject: [getContainerToken(mongoContainerName), MONGO_MODULE_OPTIONS],
        };

        const dbProvider = {
            provide: getDbToken(mongoContainerName),
            useFactory: (
                mongoModuleOptions: MongoModuleOptions,
                client: MongoClient,
            ) => client.db(mongoModuleOptions.dbName),
            inject: [MONGO_MODULE_OPTIONS, getClientToken(mongoContainerName)],
        };

        const asyncProviders = this.createAsyncProviders(options);

        return {
            module: MongoCoreModule,
            imports: options.imports,
            providers: [
                ...asyncProviders,
                clientProvider,
                dbProvider,
                containerNameProvider,
                connectionContainerProvider,
            ],
            exports: [clientProvider, dbProvider],
        };
    }

    async onModuleDestroy() {
        const clientsMap: Map<any, MongoClient> = this.moduleRef.get<
            Map<any, MongoClient>
        >(getContainerToken(this.containerName));

        if (clientsMap) {
            await Promise.all(
                [...clientsMap.values()].map(connection => connection.close()),
            );
        }
    }

    private static createAsyncProviders(
        options: MongoModuleAsyncOptions,
    ): Provider[] {
        if (options.useExisting || options.useFactory) {
            return [this.createAsyncOptionsProvider(options)];
        } else if (options.useClass) {
            return [
                this.createAsyncOptionsProvider(options),
                {
                    provide: options.useClass,
                    useClass: options.useClass,
                },
            ];
        } else {
            return [];
        }
    }

    private static createAsyncOptionsProvider(
        options: MongoModuleAsyncOptions,
    ): Provider {
        if (options.useFactory) {
            return {
                provide: MONGO_MODULE_OPTIONS,
                useFactory: options.useFactory,
                inject: options.inject || [],
            };
        } else if (options.useExisting) {
            return {
                provide: MONGO_MODULE_OPTIONS,
                useFactory: async (optionsFactory: MongoOptionsFactory) =>
                    await optionsFactory.createMongoOptions(),
                inject: [options.useExisting],
            };
        } else if (options.useClass) {
            return {
                provide: MONGO_MODULE_OPTIONS,
                useFactory: async (optionsFactory: MongoOptionsFactory) =>
                    await optionsFactory.createMongoOptions(),
                inject: [options.useClass],
            };
        } else {
            throw new Error('Invalid MongoModule options');
        }
    }
}


来源:https://stackoverflow.com/questions/51385738/nestjs-database-connection-typeorm-by-request-subdomain

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!