Explorar o código

feat(core): Allow specifying transaction isolation level (#2116)

Jan Doms %!s(int64=2) %!d(string=hai) anos
pai
achega
bf2b1f54d5

+ 15 - 1
packages/core/src/api/decorators/transaction.decorator.ts

@@ -31,6 +31,19 @@ export const TRANSACTION_MODE_METADATA_KEY = '__transaction_mode__';
  */
 export type TransactionMode = 'auto' | 'manual';
 
+export const TRANSACTION_ISOLATION_LEVEL_METADATA_KEY = '__transaction_isolation_level__';
+/**
+ * @description
+ * Transactions can be run at different isolation levels. The default is undefined, which
+ * falls back to the default of your database. See the documentation of your database for more
+ * information on available isolation levels.
+ * 
+ * @default undefined
+ * @docsCategory request
+ * @docsPage Transaction Decorator
+ */
+export type TransactionIsolationLevel = 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE';
+
 /**
  * @description
  * Runs the decorated method in a TypeORM transaction. It works by creating a transactional
@@ -61,9 +74,10 @@ export type TransactionMode = 'auto' | 'manual';
  * @docsPage Transaction Decorator
  * @docsWeight 0
  */
-export const Transaction = (transactionMode: TransactionMode = 'auto') => {
+export const Transaction = (transactionMode: TransactionMode = 'auto', transactionIsolationLevel?: TransactionIsolationLevel) => {
     return applyDecorators(
         SetMetadata(TRANSACTION_MODE_METADATA_KEY, transactionMode),
+        SetMetadata(TRANSACTION_ISOLATION_LEVEL_METADATA_KEY, transactionIsolationLevel),
         UseInterceptors(TransactionInterceptor),
     );
 };

+ 8 - 3
packages/core/src/api/middleware/transaction-interceptor.ts

@@ -7,7 +7,7 @@ import { REQUEST_CONTEXT_KEY, REQUEST_CONTEXT_MAP_KEY } from '../../common/const
 import { TransactionWrapper } from '../../connection/transaction-wrapper';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { parseContext } from '../common/parse-context';
-import { TransactionMode, TRANSACTION_MODE_METADATA_KEY } from '../decorators/transaction.decorator';
+import { TransactionMode, TRANSACTION_MODE_METADATA_KEY, TransactionIsolationLevel, TRANSACTION_ISOLATION_LEVEL_METADATA_KEY } from '../decorators/transaction.decorator';
 
 /**
  * @description
@@ -20,7 +20,7 @@ export class TransactionInterceptor implements NestInterceptor {
         private connection: TransactionalConnection,
         private transactionWrapper: TransactionWrapper,
         private reflector: Reflector,
-    ) {}
+    ) { }
 
     intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
         const { isGraphQL, req } = parseContext(context);
@@ -31,7 +31,11 @@ export class TransactionInterceptor implements NestInterceptor {
                 TRANSACTION_MODE_METADATA_KEY,
                 context.getHandler(),
             );
-            
+            const transactionIsolationLevel = this.reflector.get<TransactionIsolationLevel | undefined>(
+                TRANSACTION_ISOLATION_LEVEL_METADATA_KEY,
+                context.getHandler(),
+            );
+
             return of(
                 this.transactionWrapper.executeInTransaction(
                     ctx,
@@ -41,6 +45,7 @@ export class TransactionInterceptor implements NestInterceptor {
                         return next.handle()
                     },
                     transactionMode,
+                    transactionIsolationLevel,
                     this.connection.rawConnection,
                 )
             );

+ 7 - 6
packages/core/src/connection/transaction-wrapper.ts

@@ -4,7 +4,7 @@ import { Connection, EntityManager, QueryRunner } from 'typeorm';
 import { TransactionAlreadyStartedError } from 'typeorm/error/TransactionAlreadyStartedError';
 
 import { RequestContext } from '../api/common/request-context';
-import { TransactionMode } from '../api/decorators/transaction.decorator';
+import { TransactionIsolationLevel, TransactionMode } from '../api/decorators/transaction.decorator';
 import { TRANSACTION_MANAGER_KEY } from '../common/constants';
 
 /**
@@ -27,16 +27,17 @@ export class TransactionWrapper {
         originalCtx: RequestContext,
         work: (ctx: RequestContext) => Observable<T> | Promise<T>,
         mode: TransactionMode,
+        isolationLevel: TransactionIsolationLevel | undefined,
         connection: Connection,
     ): Promise<T> {
         // Copy to make sure original context will remain valid after transaction completes
         const ctx = originalCtx.copy();
 
         const entityManager: EntityManager | undefined = (ctx as any)[TRANSACTION_MANAGER_KEY];
-        const queryRunner = entityManager?.queryRunner || connection.createQueryRunner();
+        const queryRunner = entityManager ?.queryRunner || connection.createQueryRunner();
 
         if (mode === 'auto') {
-            await this.startTransaction(queryRunner);
+            await this.startTransaction(queryRunner, isolationLevel);
         }
         (ctx as any)[TRANSACTION_MANAGER_KEY] = queryRunner.manager;
 
@@ -66,7 +67,7 @@ export class TransactionWrapper {
             }
             throw error;
         } finally {
-            if (!queryRunner.isTransactionActive 
+            if (!queryRunner.isTransactionActive
                 && queryRunner.isReleased === false) {
                 // There is a check for an active transaction
                 // because this could be a nested transaction (savepoint).
@@ -80,7 +81,7 @@ export class TransactionWrapper {
      * Attempts to start a DB transaction, with retry logic in the case that a transaction
      * is already started for the connection (which is mainly a problem with SQLite/Sql.js)
      */
-    private async startTransaction(queryRunner: QueryRunner) {
+    private async startTransaction(queryRunner: QueryRunner, isolationLevel: TransactionIsolationLevel | undefined) {
         const maxRetries = 25;
         let attempts = 0;
         let lastError: any;
@@ -88,7 +89,7 @@ export class TransactionWrapper {
         // Returns false if a transaction is already in progress
         async function attemptStartTransaction(): Promise<boolean> {
             try {
-                await queryRunner.startTransaction();
+                await queryRunner.startTransaction(isolationLevel);
                 return true;
             } catch (err) {
                 lastError = err;

+ 8 - 7
packages/core/src/connection/transactional-connection.ts

@@ -22,6 +22,7 @@ import { VendureEntity } from '../entity/base/base.entity';
 import { removeCustomFieldsWithEagerRelations } from './remove-custom-fields-with-eager-relations';
 import { TransactionWrapper } from './transaction-wrapper';
 import { GetEntityOrThrowOptions } from './types';
+import { TransactionIsolationLevel } from '../api/decorators/transaction.decorator';
 
 /**
  * @description
@@ -40,7 +41,7 @@ export class TransactionalConnection {
     constructor(
         @InjectConnection() private connection: Connection,
         private transactionWrapper: TransactionWrapper,
-    ) {}
+    ) { }
 
     /**
      * @description
@@ -147,7 +148,7 @@ export class TransactionalConnection {
             ctx = RequestContext.empty();
             work = ctxOrWork;
         }
-        return this.transactionWrapper.executeInTransaction(ctx, work, 'auto', this.rawConnection);
+        return this.transactionWrapper.executeInTransaction(ctx, work, 'auto', undefined, this.rawConnection);
     }
 
     /**
@@ -155,10 +156,10 @@ export class TransactionalConnection {
      * Manually start a transaction if one is not already in progress. This method should be used in
      * conjunction with the `'manual'` mode of the {@link Transaction} decorator.
      */
-    async startTransaction(ctx: RequestContext) {
+    async startTransaction(ctx: RequestContext, isolationLevel?: TransactionIsolationLevel) {
         const transactionManager = this.getTransactionManager(ctx);
-        if (transactionManager?.queryRunner?.isTransactionActive === false) {
-            await transactionManager.queryRunner.startTransaction();
+        if (transactionManager ?.queryRunner ?.isTransactionActive === false) {
+            await transactionManager.queryRunner.startTransaction(isolationLevel);
         }
     }
 
@@ -171,7 +172,7 @@ export class TransactionalConnection {
      */
     async commitOpenTransaction(ctx: RequestContext) {
         const transactionManager = this.getTransactionManager(ctx);
-        if (transactionManager?.queryRunner?.isTransactionActive) {
+        if (transactionManager ?.queryRunner ?.isTransactionActive) {
             await transactionManager.queryRunner.commitTransaction();
         }
     }
@@ -184,7 +185,7 @@ export class TransactionalConnection {
      */
     async rollBackTransaction(ctx: RequestContext) {
         const transactionManager = this.getTransactionManager(ctx);
-        if (transactionManager?.queryRunner?.isTransactionActive) {
+        if (transactionManager ?.queryRunner ?.isTransactionActive) {
             await transactionManager.queryRunner.rollbackTransaction();
         }
     }