diff --git a/packages/core/src/api/common/request-context.ts b/packages/core/src/api/common/request-context.ts index e94b90fafb..1987449bb7 100644 --- a/packages/core/src/api/common/request-context.ts +++ b/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; + } } diff --git a/packages/core/src/connection/transactional-connection.ts b/packages/core/src/connection/transactional-connection.ts index 3b3c331663..e2d82f2cbd 100644 --- a/packages/core/src/connection/transactional-connection.ts +++ b/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( ctx: RequestContext | undefined, target: ObjectType | EntitySchema | string, + options?: { + replicationMode?: ReplicationMode; + }, ): Repository; + /** + * @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( ctxOrTarget: RequestContext | ObjectType | EntitySchema | string | undefined, maybeTarget?: ObjectType | EntitySchema | string, + options?: { + replicationMode?: ReplicationMode; + }, ): Repository { 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!); }