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å:
- legg til alle tilkoblingsinnstillinger i ormconfig-filen
opprett mellomvare på alle ruter for å injisere underdomene i
res.locals(instansnavn) og opprette/advar typeorm-tilkoblingimport { 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(); }; } }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) ) }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
...
}
}
