Просмотр исходного кода

feat(core): Add replicationMode for ctx and getRepository (#2746)

Eugene Nitsenko 1 год назад
Родитель
Сommit
60cdae3a7c

+ 40 - 0
packages/core/src/api/common/request-context.ts

@@ -3,6 +3,7 @@ import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
 import { isObject } from '@vendure/common/lib/shared-utils';
 import { Request } from 'express';
 import { TFunction } from 'i18next';
+import { ReplicationMode } from 'typeorm';
 
 import { idsAreEqual } from '../../common/utils';
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
@@ -32,6 +33,11 @@ export type SerializedRequestContext = {
  * the active Channel, and so on. In addition, the {@link TransactionalConnection} relies on the
  * presence of the RequestContext object in order to correctly handle per-request database transactions.
  *
+ * The RequestContext also provides mechanisms for managing the database replication mode via the
+ * {@link setReplicationMode} method and the {@link replicationMode} getter. This allows for finer control
+ * over whether database queries within the context should be executed against the master or a replica
+ * database, which can be particularly useful in distributed database environments.
+ *
  * @example
  * ```ts
  * \@Query()
@@ -39,6 +45,15 @@ export type SerializedRequestContext = {
  *   return this.myService.getData(ctx);
  * }
  * ```
+ *
+ * @example
+ * ```ts
+ * \@Query()
+ * myMutation(\@Ctx() ctx: RequestContext) {
+ *   ctx.setReplicationMode('master');
+ *   return this.myService.getData(ctx);
+ * }
+ * ```
  * @docsCategory request
  */
 export class RequestContext {
@@ -51,6 +66,7 @@ export class RequestContext {
     private readonly _translationFn: TFunction;
     private readonly _apiType: ApiType;
     private readonly _req?: Request;
+    private _replicationMode?: ReplicationMode;
 
     /**
      * @internal
@@ -284,4 +300,28 @@ export class RequestContext {
         }
         return copySimpleFieldsToDepth(req, 1);
     }
+
+    /**
+     * @description
+     * Sets the replication mode for the current RequestContext. This mode determines whether the operations
+     * within this context should interact with the master database or a replica. Use this method to explicitly
+     * define the replication mode for the context.
+     *
+     * @param mode - The replication mode to be set (e.g., 'master' or 'replica').
+     */
+    setReplicationMode(mode: ReplicationMode): void {
+        this._replicationMode = mode;
+    }
+
+    /**
+     * @description
+     * Gets the current replication mode of the RequestContext. If no replication mode has been set,
+     * it returns `undefined`. This property indicates whether the context is configured to interact with
+     * the master database or a replica.
+     *
+     * @returns The current replication mode, or `undefined` if none is set.
+     */
+    get replicationMode(): ReplicationMode | undefined {
+        return this._replicationMode;
+    }
 }

+ 41 - 2
packages/core/src/connection/transactional-connection.ts

@@ -11,6 +11,7 @@ import {
     ObjectType,
     Repository,
     SelectQueryBuilder,
+    ReplicationMode,
 } from 'typeorm';
 
 import { RequestContext } from '../api/common/request-context';
@@ -70,25 +71,63 @@ export class TransactionalConnection {
      * Returns a TypeORM repository which is bound to any existing transactions. It is recommended to _always_ pass
      * the RequestContext argument when possible, otherwise the queries will be executed outside of any
      * ongoing transactions which have been started by the {@link Transaction} decorator.
+     *
+     * The `options` parameter allows specifying additional configurations, such as the `replicationMode`,
+     * which determines whether the repository should interact with the master or replica database.
+     *
+     * @param ctx - The RequestContext, which ensures the repository is aware of any existing transactions.
+     * @param target - The entity type or schema for which the repository is returned.
+     * @param options - Additional options for configuring the repository, such as the `replicationMode`.
+     *
+     * @returns A TypeORM repository for the specified entity type.
      */
     getRepository<Entity extends ObjectLiteral>(
         ctx: RequestContext | undefined,
         target: ObjectType<Entity> | EntitySchema<Entity> | string,
+        options?: {
+            replicationMode?: ReplicationMode;
+        },
     ): Repository<Entity>;
+    /**
+     * @description
+     * Returns a TypeORM repository. Depending on the parameters passed, it will either be transaction-aware
+     * or not. If `RequestContext` is provided, the repository is bound to any ongoing transactions. The
+     * `options` parameter allows further customization, such as selecting the replication mode (e.g., 'master').
+     *
+     * @param ctxOrTarget - Either the RequestContext, which binds the repository to ongoing transactions, or the entity type/schema.
+     * @param maybeTarget - The entity type or schema for which the repository is returned (if `ctxOrTarget` is a RequestContext).
+     * @param options - Additional options for configuring the repository, such as the `replicationMode`.
+     *
+     * @returns A TypeORM repository for the specified entity type.
+     */
     getRepository<Entity extends ObjectLiteral>(
         ctxOrTarget: RequestContext | ObjectType<Entity> | EntitySchema<Entity> | string | undefined,
         maybeTarget?: ObjectType<Entity> | EntitySchema<Entity> | string,
+        options?: {
+            replicationMode?: ReplicationMode;
+        },
     ): Repository<Entity> {
         if (ctxOrTarget instanceof RequestContext) {
             const transactionManager = this.getTransactionManager(ctxOrTarget);
             if (transactionManager) {
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 return transactionManager.getRepository(maybeTarget!);
-            } else {
+            }
+
+            if (ctxOrTarget.replicationMode === 'master' || options?.replicationMode === 'master') {
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                return this.rawConnection.getRepository(maybeTarget!);
+                return this.dataSource.createQueryRunner('master').manager.getRepository(maybeTarget!);
             }
+
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            return this.rawConnection.getRepository(maybeTarget!);
         } else {
+            if (options?.replicationMode === 'master') {
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                return this.dataSource
+                    .createQueryRunner(options.replicationMode)
+                    .manager.getRepository(maybeTarget!);
+            }
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             return this.rawConnection.getRepository(ctxOrTarget ?? maybeTarget!);
         }