我正在尝试通过Nest / TypeORM构建SAAS产品,并且需要按子域配置/更改数据库连接。
customer1.domain.com => connect to customer1 database
customer2.domain.com => connect to customer2 database
x.domain.com => connect to x database
我该怎么做?使用拦截器或请求上下文(或Zone.js)吗?
我不知道如何开始。有人已经这样做了吗?
WIP:我目前在做什么:
在所有路由上创建中间件,以将子域注入res.locals
(实例名称)并创建/警告typeorm连接
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();
};
}
}
在Controller中:从@Response获取实例名称并将其传递给我的服务
@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)
)
}
在服务中:获取给定实例的TypeORM Manager,并通过存储库查询数据库
@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();
}
这似乎可行,但我不确定几乎所有内容:
处理response.send()+ Promise + await(s)+在任何地方传递子域都不是一件令人愉快的事情。
是否可以将子域直接添加到我的服务中?
是否可以将正确的子域连接/存储库直接添加到我的服务中,并将其注入到我的控制器中?
答案 0 :(得分:1)
我想出了另一种解决方案。
我创建了一个中间件来获取特定租户的连接:
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();
};
}
我将其添加到main.ts:
const app = await NestFactory.create(AppModule);
app.use(tenancyConnection(AppModule));
要访问连接,您可以通过以下方式扩展任何服务:
export class TenancyConnection {
getConnection(): Connection {
return Reflect.getMetadata('__tenancyConnection__', AppModule);
}
}
这仍然是草稿,但是使用此解决方案,您可以在运行时为每个租户添加,删除和编辑连接。 希望对您有帮助。
答案 1 :(得分:1)
我从yoh的解决方案中得到启发,但根据NestJS的新功能对其进行了一些调整。结果是更少的代码。
1)我创建了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();
}
}
main.ts中的2)将其用于所有路由
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(new DatabaseMiddleware().use);
...
3)在服务中检索连接
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
...
}
}
答案 2 :(得分:1)
您应该使用范围为REQUEST
的自定义提供程序。
租户提供商
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 { }
服务等级
然后在您的服务上可以像这样获得连接:
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();
}
}
您可以在以下多租户文章中获得更多信息:https://tech.canyonlegal.com/multitenancy-with-nestjs-typeorm-postgres
答案 3 :(得分:0)
最好的方法是使用动态模块,就像您对请求范围所做的那样,以获取ORM
连接并使其特定于连接。
一个非常简单的示例可能是这样的:
const tenancyFactory: Provider = {
provide: NEST_MYSQL2_TENANCY,
scope: 'REQUEST',
useFactory: async (mysql: Mysql, options: MysqlTenancyOption, req: Request): Promise<any> => {
console.log("TENANCY FACTORY");
const executer = function (mysqlPool: Mysql): MysqlExecuter {
return {
db: function (dbName: string): MysqlRunner {
return {
run: async function (sqlString: string) {
const q = `\nUSE ${dbName};\n` +
sqlString.replace("; ", ";\n");
if (options.debug) {
tLogger.verbose(q);
}
const [[_, ...queryResult], __] = await mysqlPool.query(q)
return queryResult as any;
}
}
}
}
}
return executer(mysql);
},
inject: [NEST_MYSQL2_CONNECTION, NEST_MYSQL2_TENANCY_OPTION],
};
@Global()
@Module({
providers: [tenancyFactory],
exports: [tenancyFactory],
})
export class MultiTenancyModule {
constructor(
) { }
public static register(options: MysqlTenancyOption): DynamicModule {
return {
module: MultiTenancyModule,
providers: [{
provide: NEST_MYSQL2_TENANCY_OPTION,
useValue: options
}]
};
}
}
在此示例中,我具有用户mysql2-nestjs模块,但是您可以使用自己的ORM
创建tenancyFactory
您可以在下面的ling的有效解决方案中找到此样本 https://github.com/golkhandani/multi-tenancy/blob/main/test/src/tenancy.module.ts
答案 4 :(得分:-1)
我为此nest-mongodb写了一个实现,请检查一下可能有帮助。
类似的问题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');
}
}
}