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 :
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();
};
}
}
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)
)
}
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 :
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 ?
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.
You should use a custom provider with REQUEST
scope.
Tenancy provider
import { Global, Module, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Connection, createConnection, getConnectionManager } from 'typeorm';
const connectionFactory = {
provide: 'CONNECTION',
scope: Scope.REQUEST,
useFactory: async (req) => {
const instance = req.headers.host.split('.')[0]
if (instance) {
const connectionManager = getConnectionManager();
if (connectionManager.has(instance)) {
const connection = connectionManager.get(instance);
return Promise.resolve(connection.isConnected ? connection : connection.connect());
}
return createConnection({
...tenantsOrmconfig,
entities: [...(tenantsOrmconfig as any).entities, ...(ormconfig as any).entities],
name: instance,
type: 'postgres',
schema: instance
});
}
},
inject: [REQUEST]
};
@Global()
@Module({
providers: [connectionFactory],
exports: ['CONNECTION']
})
export class TenancyModule { }
Service class
Then on your services you can get the connection like that:
import { Injectable} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GameEntity } from './game.entity';
@Injectable()
export class MyService {
constructor(
@Inject('CONNECTION') connection
) {
this.myRepository = connection.getRepository(GameEntity);
}
findAll(): Promise<GameEntity[]> {
return this.myRepository.find();
}
}
You can get more informations in the following multi-tenants article: https://tech.canyonlegal.com/multitenancy-with-nestjs-typeorm-postgres
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
...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With