NESTJSでマルチテナントをセットアップするにはどうすればよいですか

4
Hiro Palacios 2019-05-11 12:49.

サブドメイン(マルチテナント)に基づいて任意のデータベースに接続したいのですが、どうすればよいかわかりません。

アプリの起動時にコードが実行されますが、サブドメインに基づいてデータソースを変更する方法がわかりません。

PS:リクエストごとにミドルウェアを作成しましたが、ソースを変更する方法がわかりません。

DB用に次のコードがあります。

import { connect, createConnection } from 'mongoose';
import { SERVER_CONFIG, DB_CONNECTION_TOKEN } from '../server.constants';

 const opts = {
    useCreateIndex: true,
    useNewUrlParser: true,
    keepAlive: true,
    socketTimeoutMS: 30000,
    poolSize: 100,
    reconnectTries: Number.MAX_VALUE,
    reconnectInterval: 500,
    autoReconnect: true,
  };
export const databaseProviders = [
  {
    provide: DB_CONNECTION_TOKEN,
    useFactory: async () => {
      try {
        console.log(`Connecting to ${ SERVER_CONFIG.db }`); return await createConnection(`${SERVER_CONFIG.db}`, opts);
      } catch (ex) {
        console.log(ex);
      }

    },
  }
];

サブドメイン(マルチテナント)に基づいて、リクエストごとにデータソースを変更したい

2 answers

12
Sandeep K Nair 2020-01-16 22:35.

これが私がマングースで使用した解決策です

  1. TenantsService アプリケーション内のすべてのテナントを管理するために使用されます
@Injectable()
export class TenantsService {
    constructor(
        @InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
    ) {}

    /**
     * Save tenant data
     *
     * @param {CreateTenantDto} createTenant
     * @returns {Promise<ITenant>}
     * @memberof TenantsService
     */
    async create(createTenant: CreateTenantDto): Promise<ITenant> {
        try {
            const dataToPersist = new this.tenantModel(createTenant);
            // Persist the data
            return await dataToPersist.save();
        } catch (error) {
            throw new HttpException(error, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Find details of a tenant by name
     *
     * @param {string} name
     * @returns {Promise<ITenant>}
     * @memberof TenantsService
     */
    async findByName(name: string): Promise<ITenant> {
        return await this.tenantModel.findOne({ name });
    }
}

  1. TenantAwareMiddlewaretenant idリクエストコンテキストから取得するミドルウェア。ここで独自のロジックを作成してtenant id、リクエストヘッダーまたはリクエストURLサブドメインからを抽出できます。リクエストヘッダーの抽出方法を次に示します。

サブドメインを抽出したい場合は、をRequest呼び出してオブジェクトからサブドメインを抽出することで同じことができますreq.subdomains。これにより、サブドメインのリストが表示され、そこから探しているものを取得できます。

@Injectable()
export class TenantAwareMiddleware implements NestMiddleware {
    async use(req: Request, res: Response, next: NextFunction) {
        // Extract from the request object
        const { subdomains, headers } = req;

        // Get the tenant id from header
        const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];

        if (!tenantId) {
            throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
        }

        // Set the tenant id in the header
        req['tenantId'] = tenantId.toString();

        next();
    }
}
  1. TenantConnectionこのクラスは、を使用して新しい接続を作成するために使用されtenant id、使用可能な既存の接続がある場合は、同じ接続を返します(追加の接続の作成を回避するため)。
@Injectable()
export class TenantConnection {
    private _tenantId: string;

    constructor(
        private tenantService: TenantsService,
        private configService: ConfigService,
    ) {}

    /**
     * Set the context of the tenant
     *
     * @memberof TenantConnection
     */
    set tenantId(tenantId: string) {
        this._tenantId = tenantId;
    }

    /**
     * Get the connection details
     *
     * @param {ITenant} tenant
     * @returns
     * @memberof TenantConnection
     */
    async getConnection(): Connection {
        // Get the tenant details from the database
        const tenant = await this.tenantService.findByName(this._tenantId);

        // Validation check if tenant exist
        if (!tenant) {
            throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
        }

        // Get the underlying mongoose connections
        const connections: Connection[] = mongoose.connections;

        // Find existing connection
        const foundConn = connections.find((con: Connection) => {
            return con.name === `tenantDB_${tenant.name}`; }); // Check if connection exist and is ready to execute if (foundConn && foundConn.readyState === 1) { return foundConn; } // Create a new connection return await this.createConnection(tenant); } /** * Create new connection * * @private * @param {ITenant} tenant * @returns {Connection} * @memberof TenantConnection */ private async createConnection(tenant: ITenant): Promise<Connection> { // Create or Return a mongo connection return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
    }
}

  1. TenantConnectionFactoryこれはtenant id、接続の作成を支援するカスタムプロバイダーです。
// Tenant creation factory
export const TenantConnectionFactory = [
    {
        provide: 'TENANT_CONTEXT',
        scope: Scope.REQUEST,
        inject: [REQUEST],
        useFactory: (req: Request): ITenantContext => {
            const { tenantId } = req as any;
            return new TenantContext(tenantId);
        },
    },
    {
        provide: 'TENANT_CONNECTION',
        useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose>  => {
            // Set tenant context
            connection.tenantId = context.tenantId;

            // Return the connection
            return connection.getConnection();
        },
        inject: ['TENANT_CONTEXT', TenantConnection],
    },
];
  1. TenantsModule-ここでTenantConnectionFactoryは、プロバイダーとして追加され、他のモジュール内で使用するためにエクスポートされていることがわかります。
@Module({
  imports: [
    CoreModule,
  ],
  controllers: [TenantsController],
  providers: [
    TenantsService,
    TenantConnection,
    ...TenantConnectionFactory,
  ],
  exports: [
    ...TenantConnectionFactory,
  ],
})
export class TenantsModule {}
  1. TenantModelProviders -テナントモデルはテナント接続に依存するため、モデルはプロバイダーを介して定義し、モデルを初期化するモジュール内に含める必要があります。
export const TenantModelProviders = [
    {
        provide: 'USER_MODEL',
        useFactory: (connection: Connection) => connection.model('User', UserSchema),
        inject: ['TENANT_CONNECTION'],
    },
];
  1. UsersModule-このクラスはモデルを使用します。ここでは、tenanddbルートに作用するようにミドルウェアが構成されていることも確認できます。この場合、すべてのuserルートはテナントの一部であり、テナントデータベースによって提供されます。
@Module({
  imports: [
    CoreModule,
    TenantsModule,
  ],
  providers: [
    UsersService,
    ...TenantModelProviders,
  ],
  controllers: [UsersController],
})
export class UsersModule implements NestModule {
  configure(context: MiddlewareConsumer) {
    context.apply(TenantAwareMiddleware).forRoutes('/users');
  }
}
  1. UsersService -ユーザーモジュールからテナントデータベースにアクセスする実装例
@Injectable()
export class UsersService {

    constructor(
        @Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
        @Inject('USER_MODEL') private userModel: Model<IUser>,
    ) {
        Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
    }

    /**
     * Create a new user
     *
     * @param {CreateUserDto} user
     * @returns {Promise<IUser>}
     * @memberof UsersService
     */
    async create(user: CreateUserDto): Promise<IUser> {
        try {
            const dataToPersist = new this.userModel(user);
            // Persist the data
            return await dataToPersist.save();
        } catch (error) {
            throw new HttpException(error, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Get the list of all users
     *
     * @returns {Promise<IUser>}
     * @memberof UsersService
     */
    async findAll(): Promise<IUser> {
        return await this.userModel.find({});
    }
}

4
L. Lenz 2019-05-16 03:08.

NestJSセットアップ用のマルチテナンシーセットアップもあります。
要求に応じて、使用するデータソースを決定するミドルウェアを使用できます。この例では、NestJSにかなりよく統合されているTypeORMを使用しています。TypeORMパッケージにはいくつかの便利な機能があります。

ミドルウェア

export class AppModule {
  constructor(private readonly connection: Connection) {
  }

  configure(consumer: MiddlewareConsumer): void {
    consumer
      .apply(async (req, res, next) => {
        try {
          getConnection(tenant);
          next();
        } catch (e) {
          const tenantRepository = this.connection.getRepository(tenant);
          const tenant = await tenantRepository.findOne({ name: tenant });
          if (tenant) {
            const createdConnection: Connection = await createConnection(options);
            if (createdConnection) {
              next();
            } else {
              throw new CustomNotFoundException(
                'Database Connection Error',
                'There is a Error with the Database!',
              );
            }
          }
        }
      }).forRoutes('*');
   }

これは私たちのミドルウェアの例です。TypeORMは内部で接続を管理しています。したがって、最初に試みることは、その特定のテナントの接続をロードすることです。ある場合は、それ以外の場合は作成するだけです。ここでの良い点は、一度作成された接続はTypeORM接続マネージャーで引き続き使用できることです。このようにして、ルートには常に接続があります。
ルートでは、テナントのIDが必要です。私たちの場合、それはURLから抽出された単なる文字列です。値が何であれ、ミドルウェア内のリクエストオブジェクトにバインドできます。コントローラで、その値を再度抽出し、サービスに渡します。次に、テナントと準備完了のリポジトリをロードする必要があります。

サービスクラス

@Injectable()
export class SampleService {

  constructor() {}

  async getTenantRepository(tenant: string): Promise<Repository<Entity>> {
    try {
      const connection: Connection = await getConnection(tenant);
      return connection.getRepository(Property);
    } catch (e) {
      throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
    }
  }

  async findOne(params: Dto, tenant: string) {

    const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);

    return await propertyRepository.findOne({ where: params });

  }

これが、アプリケーションでのサービスの外観です。

うまくいけば、これはあなたに刺激を与え、あなたがあなたの問題に取り組むようになるでしょう:)

Related questions

MORE COOL STUFF

「水曜日」シーズン1の中心には大きなミステリーがあります

「水曜日」シーズン1の中心には大きなミステリーがあります

Netflixの「水曜日」は、典型的な10代のドラマ以上のものであり、実際、シーズン1にはその中心に大きなミステリーがあります.

ボディーランゲージの専門家は、州訪問中にカミラ・パーカー・ボウルズが輝くことを可能にした微妙なケイト・ミドルトンの動きを指摘しています

ボディーランゲージの専門家は、州訪問中にカミラ・パーカー・ボウルズが輝くことを可能にした微妙なケイト・ミドルトンの動きを指摘しています

ケイト・ミドルトンは、州の夕食会と州の訪問中にカミラ・パーカー・ボウルズからスポットライトを奪いたくなかった、と専門家は言う.

一部のファンがハリー・スタイルズとオリビア・ワイルドの「非常に友好的な」休憩が永続的であることを望んでいる理由

一部のファンがハリー・スタイルズとオリビア・ワイルドの「非常に友好的な」休憩が永続的であることを望んでいる理由

一部のファンが、オリビア・ワイルドが彼女とハリー・スタイルズとの間の「難しい」が「非常に友好的」な分割を恒久的にすることを望んでいる理由を見つけてください.

エリザベス女王の死後、ケイト・ミドルトンはまだ「非常に困難な時期」を過ごしている、と王室の専門家が明らかにする 

エリザベス女王の死後、ケイト・ミドルトンはまだ「非常に困難な時期」を過ごしている、と王室の専門家が明らかにする&nbsp;

エリザベス女王の死後、ケイト・ミドルトンが舞台裏で「非常に困難な時期」を過ごしていたと伝えられている理由を調べてください.

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セント ヘレナ島のジェイコブズ ラダーは 699 段の真っ直ぐ上る階段で、頂上に到達すると証明書が発行されるほどの難易度です。

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

バイオニック読書はあなたをより速く読むことができますか?

バイオニック読書はあなたをより速く読むことができますか?

BionicReadingアプリの人気が爆発的に高まっています。しかし、それは本当にあなたを速読術にすることができますか?

独自のGoogleHomeジュークボックスを作成する方法

独自のGoogleHomeジュークボックスを作成する方法

スマートホームサウンドシステムをワンランク上に上げたいとお考えの場合は、これが楽しい方法です。少し余分な作業といくつかのRFIDテクノロジーを使用して、次のパーティーで感動すること間違いなしの独自のカード駆動ジュークボックスを構築できます。

はい、私たちの生活のための行進は黒人についてでした、そしてそれは時間についてです

はい、私たちの生活のための行進は黒人についてでした、そしてそれは時間についてです

コモンとアンドラデイは、2018年3月24日、ワシントンDCで開催されるマーチフォーアワーライフズラリーで、シェハン枢機卿学校合唱団のメンバーと「スタンドアップフォーサムシング」を行います。

テスラモデル3について何を知りたいですか?

テスラモデル3について何を知りたいですか?

テスラモデル3は未来的であるだけでなく、驚くほど普通に感じます。しかし、実際には何が正常ですか?前回はモデル3を数時間しか運転で​​きませんでしたが、今回は週末を過ごしました。

今週のビジネス:スイッチポートの本当の問題

今週のビジネス:スイッチポートの本当の問題

見積もり| 「スイッチはPS4やXboxOneほど強力ではありません。誰もがそれを知っています。

ケイト・ミドルトンとウィリアム王子は、彼らが子供たちと行っているスパイをテーマにした活動を共有しています

ケイト・ミドルトンとウィリアム王子は、彼らが子供たちと行っているスパイをテーマにした活動を共有しています

ケイト・ミドルトンとウィリアム王子は、子供向けのパズルの本の序文を書き、ジョージ王子、シャーロット王女、ルイ王子と一緒にテキストを読むと述べた.

事故で押しつぶされたスイカは、動物を喜ばせ水分補給するために野生生物保護団体に寄付されました

事故で押しつぶされたスイカは、動物を喜ばせ水分補給するために野生生物保護団体に寄付されました

Yak's Produce は、数十個のつぶれたメロンを野生動物のリハビリ専門家であるレスリー グリーンと彼女のルイジアナ州の救助施設で暮らす 42 匹の動物に寄付しました。

デミ・ロヴァートは、新しいミュージシャンのボーイフレンドと「幸せで健康的な関係」にあります: ソース

デミ・ロヴァートは、新しいミュージシャンのボーイフレンドと「幸せで健康的な関係」にあります: ソース

8 枚目のスタジオ アルバムのリリースに向けて準備を進めているデミ ロヴァートは、「スーパー グレート ガイ」と付き合っている、と情報筋は PEOPLE に確認しています。

Plathville の Kim と Olivia Plath が数年ぶりに言葉を交わすことへようこそ

Plathville の Kim と Olivia Plath が数年ぶりに言葉を交わすことへようこそ

イーサン プラスの誕生日のお祝いは、TLC のウェルカム トゥ プラスビルのシーズン 4 のフィナーレで、戦争中の母親のキム プラスと妻のオリビア プラスを結びつけました。

仕事の生産性を高める 8 つのシンプルなホーム オフィスのセットアップのアイデア

仕事の生産性を高める 8 つのシンプルなホーム オフィスのセットアップのアイデア

ホームオフィスのセットアップ術を極めよう!AppExert の開発者は、家族全員が一緒にいる場合でも、在宅勤務の技術を習得しています。祖父や曽祖父が共同家族で暮らしていた頃の記憶がよみがえりました。

2022 年、私たちのデジタル ライフはどこで終わり、「リアル ライフ」はどこから始まるのでしょうか?

20 年前のタイムトラベラーでさえ、日常生活におけるデジタルおよびインターネットベースのサービスの重要性に驚くことでしょう。MySpace、eBay、Napster などのプラットフォームは、高速化に焦点を合わせた世界がどのようなものになるかを示してくれました。

ニューロマーケティングの秘密科学

ニューロマーケティングの秘密科学

マーケティング担当者が人間の欲望を操作するために使用する、最先端の (気味が悪いと言う人もいます) メソッドを探ります。カートをいっぱいにして 3 桁の領収書を持って店を出る前に、ほんの数点の商品を買いに行ったことはありませんか? あなたは一人じゃない。

地理情報システムの日: GIS 開発者として学ぶべき最高の技術スタック

地理情報システムの日: GIS 開発者として学ぶべき最高の技術スタック

私たちが住んでいる世界を確実に理解するには、データが必要です。ただし、空間参照がない場合、このデータは地理的コンテキストがないと役に立たなくなる可能性があります。

Language