typescript - NestJS: databaseforbindelse (TypeORM) per forespørsel (underdomene)


Jeg prøver å bygge et SAAS-produkt over Nest/TypeORM, og jeg trenger å konfigurere/endret databaseforbindelse etter underdomene.

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

Hvordan kan jeg gjøre det? Med interceptorer eller request-context (eller Zone.js)?

Jeg vet ikke hvordan jeg skal starte. Er det noen som allerede gjør det?

WIP: hva jeg holder på med akkurat nå:

  1. legg til alle tilkoblingsinnstillinger i ormconfig-filen
  2. opprett mellomvare på alle ruter for å injisere underdomene i res.locals (instansnavn) og opprette/advar typeorm-tilkobling

    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. i Controller: hent instansnavn fra @Response og send det til min tjeneste

    @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. i tjeneste: få TypeORM-manager for gitt instans og spørr databasen gjennom 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();
      }
    

Det ser ut til å fungere, men jeg er ikke sikker på omtrent alt :

  • tilkoblingspooler (hvor mange kan jeg lage tilkoblinger til i min ConnectionManager?)
  • send underdomene inn i response.locals... dårlig praksis?
  • lesbarhet/forståelse/legge til mye ekstra kode...
  • bivirkninger: Jeg er redd for å dele forbindelser mellom flere underdomener
  • bivirkninger: ytelse

Det er ikke en fornøyelse å håndtere response.send() + Promise + await(s) + sende subdomene overalt...

Finnes det en måte å få subdomene direkte inn i tjenesten min?

Finnes det en måte å få korrekt underdomene Connection/Repository direkte inn i tjenesten min og injisere den i kontrolleren min?

Svar #1

Jeg kom opp med en annen løsning.

Jeg laget en middleware for å få tilkoblingen for en spesifikk leietaker:

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

Jeg la det til i main.ts:

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

For å få tilgang til tilkoblingen kan du utvide enhver tjeneste ved å:

export class TenancyConnection {

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

Det er fortsatt et utkast, men med denne løsningen kan du legge til, slette og redigere tilkoblingen for hver leietaker under kjøring. Jeg håper dette hjelper deg videre.

Svar #2

Jeg ble inspirert av yohs løsning, men jeg justerte den litt i henhold til de nye funksjonene i NestJS. Resultatet er mindre kode.

1) Jeg opprettet 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) i main.ts bruk det for alle ruter

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

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

3) Hent tilkobling i tjenesten

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
    ...
  }

}

Relaterte artikler