diff --git a/packages/medusa/src/interfaces/base-service.ts b/packages/medusa/src/interfaces/base-service.ts index d606da89e7..b87c15dd32 100644 --- a/packages/medusa/src/interfaces/base-service.ts +++ b/packages/medusa/src/interfaces/base-service.ts @@ -1,6 +1,5 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, FindOperator, In, Raw } from "typeorm" -import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" import { FindConfig, Writable } from "../types/common" type Selector = { [key in keyof TEntity]?: unknown } @@ -24,24 +23,6 @@ export class BaseService< this.container_ = container } - withTransaction(transactionManager?: EntityManager): this | TChild { - if (!transactionManager) { - return this - } - - const cloned = new (this.constructor)( - { - ...this.container_, - manager: transactionManager, - }, - this.configModule - ) - - cloned.transactionManager_ = transactionManager - - return cloned as TChild - } - /** * Used to build TypeORM queries. * @param selector The selector @@ -191,122 +172,6 @@ export class BaseService< return rawId } - shouldRetryTransaction( - err: { code: string } | Record - ): boolean { - if (!(err as { code: string })?.code) { - return false - } - const code = (err as { code: string })?.code - return code === "40001" || code === "40P01" - } - - /** - * Wraps some work within a transactional block. If the service already has - * a transaction manager attached this will be reused, otherwise a new - * transaction manager is created. - * @param work - the transactional work to be done - * @param isolationOrErrorHandler - the isolation level to be used for the work. - * @param maybeErrorHandlerOrDontFail Potential error handler - * @return the result of the transactional work - */ - async atomicPhase_( - work: (transactionManager: EntityManager) => Promise, - isolationOrErrorHandler?: - | IsolationLevel - | ((error: TError) => Promise), - maybeErrorHandlerOrDontFail?: ( - error: TError - ) => Promise - ): Promise { - let errorHandler = maybeErrorHandlerOrDontFail - let isolation: - | IsolationLevel - | ((error: TError) => Promise) - | undefined - | null = isolationOrErrorHandler - let dontFail = false - if (typeof isolationOrErrorHandler === "function") { - isolation = null - errorHandler = isolationOrErrorHandler - dontFail = !!maybeErrorHandlerOrDontFail - } - - if (this.transactionManager_) { - const doWork = async (m: EntityManager): Promise => { - this.manager_ = m - this.transactionManager_ = m - try { - return await work(m) - } catch (error) { - if (errorHandler) { - const queryRunner = this.transactionManager_.queryRunner - if (queryRunner && queryRunner.isTransactionActive) { - await queryRunner.rollbackTransaction() - } - - await errorHandler(error) - } - throw error - } - } - - return await doWork(this.transactionManager_) - } else { - const temp = this.manager_ - const doWork = async (m: EntityManager): Promise => { - this.manager_ = m - this.transactionManager_ = m - try { - const result = await work(m) - this.manager_ = temp - this.transactionManager_ = undefined - return result - } catch (error) { - this.manager_ = temp - this.transactionManager_ = undefined - throw error - } - } - - if (isolation) { - let result - try { - result = await this.manager_.transaction( - isolation as IsolationLevel, - (m) => doWork(m) - ) - return result - } catch (error) { - if (this.shouldRetryTransaction(error)) { - return this.manager_.transaction( - isolation as IsolationLevel, - (m): Promise => doWork(m) - ) - } else { - if (errorHandler) { - await errorHandler(error) - } - throw error - } - } - } - - try { - return await this.manager_.transaction((m) => doWork(m)) - } catch (error) { - if (errorHandler) { - const result = await errorHandler(error) - if (dontFail) { - return result as TResult - } - } - - throw error - } - } - } - /** * Dedicated method to set metadata. * @param obj - the entity to apply metadata to. @@ -334,4 +199,4 @@ export class BaseService< ...newData, } } -} \ No newline at end of file +} diff --git a/packages/medusa/src/interfaces/transaction-base-service.ts b/packages/medusa/src/interfaces/transaction-base-service.ts new file mode 100644 index 0000000000..cbddd4b6f5 --- /dev/null +++ b/packages/medusa/src/interfaces/transaction-base-service.ts @@ -0,0 +1,149 @@ +import { EntityManager } from "typeorm" +import { IsolationLevel } from "typeorm/driver/types/IsolationLevel" + +export abstract class TransactionBaseService< + TChild extends TransactionBaseService, + TContainer = unknown +> { + protected abstract manager_: EntityManager + protected abstract transactionManager_: EntityManager | undefined + + protected constructor( + protected readonly container: TContainer, + protected readonly configModule?: Record + ) {} + + withTransaction(transactionManager?: EntityManager): this | TChild { + if (!transactionManager) { + return this + } + + const cloned = new (this.constructor)( + { + ...this.container, + manager: transactionManager, + }, + this.configModule + ) + + cloned.transactionManager_ = transactionManager + + return cloned as TChild + } + + shouldRetryTransaction( + err: { code: string } | Record + ): boolean { + if (!(err as { code: string })?.code) { + return false + } + const code = (err as { code: string })?.code + return code === "40001" || code === "40P01" + } + + /** + * Wraps some work within a transactional block. If the service already has + * a transaction manager attached this will be reused, otherwise a new + * transaction manager is created. + * @param work - the transactional work to be done + * @param isolationOrErrorHandler - the isolation level to be used for the work. + * @param maybeErrorHandlerOrDontFail Potential error handler + * @return the result of the transactional work + */ + async atomicPhase_( + work: (transactionManager: EntityManager) => Promise, + isolationOrErrorHandler?: + | IsolationLevel + | ((error: TError) => Promise), + maybeErrorHandlerOrDontFail?: ( + error: TError + ) => Promise + ): Promise { + let errorHandler = maybeErrorHandlerOrDontFail + let isolation: + | IsolationLevel + | ((error: TError) => Promise) + | undefined + | null = isolationOrErrorHandler + let dontFail = false + if (typeof isolationOrErrorHandler === "function") { + isolation = null + errorHandler = isolationOrErrorHandler + dontFail = !!maybeErrorHandlerOrDontFail + } + + if (this.transactionManager_) { + const doWork = async (m: EntityManager): Promise => { + this.manager_ = m + this.transactionManager_ = m + try { + return await work(m) + } catch (error) { + if (errorHandler) { + const queryRunner = this.transactionManager_.queryRunner + if (queryRunner && queryRunner.isTransactionActive) { + await queryRunner.rollbackTransaction() + } + + await errorHandler(error) + } + throw error + } + } + + return await doWork(this.transactionManager_) + } else { + const temp = this.manager_ + const doWork = async (m: EntityManager): Promise => { + this.manager_ = m + this.transactionManager_ = m + try { + const result = await work(m) + this.manager_ = temp + this.transactionManager_ = undefined + return result + } catch (error) { + this.manager_ = temp + this.transactionManager_ = undefined + throw error + } + } + + if (isolation && this.manager_) { + let result + try { + result = await this.manager_.transaction( + isolation as IsolationLevel, + (m) => doWork(m) + ) + return result + } catch (error) { + if (this.shouldRetryTransaction(error)) { + return this.manager_.transaction( + isolation as IsolationLevel, + (m): Promise => doWork(m) + ) + } else { + if (errorHandler) { + await errorHandler(error) + } + throw error + } + } + } + + try { + return await this.manager_.transaction((m) => doWork(m)) + } catch (error) { + if (errorHandler) { + const result = await errorHandler(error) + if (dontFail) { + return result as TResult + } + } + + throw error + } + } + } +}