diff --git a/docs/docs/06_plugins/01_available-plugins/01-transactional/10-creating-custom-adapter.md b/docs/docs/06_plugins/01_available-plugins/01-transactional/10-creating-custom-adapter.md index 44ef6f46..d40a3edd 100644 --- a/docs/docs/06_plugins/01_available-plugins/01-transactional/10-creating-custom-adapter.md +++ b/docs/docs/06_plugins/01_available-plugins/01-transactional/10-creating-custom-adapter.md @@ -9,6 +9,7 @@ A transactional adapter is an instance of an object implementing the following i ```ts interface TransactionalAdapter { connectionToken: any; + defaultTyOptions?: Partial; optionsFactory: TransactionalOptionsAdapterFactory< TConnection, TTx, @@ -19,7 +20,9 @@ interface TransactionalAdapter { The `connectionToken` is an injection token under which the underlying database connection object is provided. -An options factory is a function that takes the injected connection object and returns the adapter options object of interface: +The `defaultTxOptions` object is the default transaction options that are used when no options are passed to the `withTransaction` call. + +An `optionFactory` is a function that takes the injected connection object and returns the adapter options object of interface: ```ts interface TransactionalAdapterOptions { @@ -35,7 +38,7 @@ interface TransactionalAdapterOptions { This object contains two methods: - `wrapWithTransaction` - a function that takes the method decorated with `@Transactional` (or a callback passed to `TransactionHost#withTransaction`) and wraps it with transaction handling logic. It should return a promise that resolves to the result of the decorated method. - The other parameter is the adapter-specific transaction `options` object and the `setTx` function which should be called with the transaction instance to make it available in the CLS context. + The other parameter is the adapter-specific transaction `options` object (which contains the transaction-specific options merged with the default ones) and the `setTx` function which should be called with the transaction instance to make it available in the CLS context. - `getFallbackInstance` - when a transactional context is not available, this method is used to return a "fallback" instance of the transaction object. This is needed for cases when the `tx` property on `TransactionHost` is accessed outside of a transactional context. @@ -117,16 +120,25 @@ export class MyTransactionalAdapterKnex // implement the property for the connection token connectionToken: any; - // In the constructor, we can decide to accept a custom options object. - // However, in this example, we'll just accept the connection token. - constructor(myKnexInstanceToken: any) { + // implement default options feature + defaultTxOptions?: Partial; + + // We can decide on a custom API for the transactional adapter. + // In this example, we just pass individual parameters, but + // a custom interface is usually preferred. + constructor( + myKnexInstanceToken: any, + defaultTxOptions: Partial, + ) { this.connectionToken = myKnexInstanceToken; + this.defaultTxOptions = defaultTxOptions; } // optionsFactory = (knexInstance: Knex) => { return { wrapWithTransaction: ( + // the options object is the transaction-specific options merged with the default ones options: Knex.TransactionConfig, fn: (...args: any[]) => Promise, setTx: (client: Knex) => void, @@ -172,7 +184,7 @@ ClsModule.forRoot({ // Don't forget to import the module which provides the knex instance imports: [KnexModule], // highlight-start - adapter: new MyTransactionalAdapterKnex(KNEX_TOKEN), + adapter: new MyTransactionalAdapterKnex(KNEX_TOKEN, { isolationLevel: 'serializable' }), // highlight-end }), ], diff --git a/packages/transactional-adapters/transactional-adapter-knex/src/lib/transactional-adapter-knex.ts b/packages/transactional-adapters/transactional-adapter-knex/src/lib/transactional-adapter-knex.ts index 39a3bc30..7f8dfce1 100644 --- a/packages/transactional-adapters/transactional-adapter-knex/src/lib/transactional-adapter-knex.ts +++ b/packages/transactional-adapters/transactional-adapter-knex/src/lib/transactional-adapter-knex.ts @@ -6,6 +6,12 @@ export interface KnexTransactionalAdapterOptions { * The injection token for the Knex instance. */ knexInstanceToken: any; + + /** + * Default options for the transaction. These will be merged with any transaction-specific options + * passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. + */ + defaultTxOptions?: Partial; } export class TransactionalAdapterKnex @@ -13,8 +19,11 @@ export class TransactionalAdapterKnex { connectionToken: any; + defaultTxOptions?: Partial; + constructor(options: KnexTransactionalAdapterOptions) { this.connectionToken = options.knexInstanceToken; + this.defaultTxOptions = options.defaultTxOptions; } optionsFactory = (knexInstance: Knex) => ({ diff --git a/packages/transactional-adapters/transactional-adapter-knex/test/transactional-adapter-knex.spec.ts b/packages/transactional-adapters/transactional-adapter-knex/test/transactional-adapter-knex.spec.ts index 173ac97d..cad4ec8e 100644 --- a/packages/transactional-adapters/transactional-adapter-knex/test/transactional-adapter-knex.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-knex/test/transactional-adapter-knex.spec.ts @@ -7,8 +7,8 @@ import { } from '@nestjs-cls/transactional'; import { Inject, Injectable, Module } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ClsModule } from 'nestjs-cls'; import Knex from 'knex'; +import { ClsModule } from 'nestjs-cls'; import { TransactionalAdapterKnex } from '../src'; const KNEX = 'KNEX'; @@ -182,3 +182,18 @@ describe('Transactional', () => { }); }); }); + +describe('Default options', () => { + it('Should correctly set default options on the adapter instance', async () => { + const adapter = new TransactionalAdapterKnex({ + knexInstanceToken: KNEX, + defaultTxOptions: { + isolationLevel: 'snapshot', + }, + }); + + expect(adapter.defaultTxOptions).toEqual({ + isolationLevel: 'snapshot', + }); + }); +}); diff --git a/packages/transactional-adapters/transactional-adapter-kysely/src/lib/transactional-adapter-kysely.ts b/packages/transactional-adapters/transactional-adapter-kysely/src/lib/transactional-adapter-kysely.ts index b76460f6..2b2aae72 100644 --- a/packages/transactional-adapters/transactional-adapter-kysely/src/lib/transactional-adapter-kysely.ts +++ b/packages/transactional-adapters/transactional-adapter-kysely/src/lib/transactional-adapter-kysely.ts @@ -6,6 +6,12 @@ export interface KyselyTransactionalAdapterOptions { * The injection token for the Kysely instance. */ kyselyInstanceToken: any; + + /** + * Default options for the transaction. These will be merged with any transaction-specific options + * passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. + */ + defaultTxOptions?: Partial; } export interface KyselyTransactionOptions { @@ -15,12 +21,16 @@ export interface KyselyTransactionOptions { } export class TransactionalAdapterKysely - implements TransactionalAdapter, Kysely, any> + implements + TransactionalAdapter, Kysely, KyselyTransactionOptions> { connectionToken: any; + defaultTxOptions?: Partial; + constructor(options: KyselyTransactionalAdapterOptions) { this.connectionToken = options.kyselyInstanceToken; + this.defaultTxOptions = options.defaultTxOptions; } optionsFactory = (kyselyDb: Kysely) => ({ diff --git a/packages/transactional-adapters/transactional-adapter-kysely/test/transactional-adapter-kysely.spec.ts b/packages/transactional-adapters/transactional-adapter-kysely/test/transactional-adapter-kysely.spec.ts index 8ce36934..c249d1b3 100644 --- a/packages/transactional-adapters/transactional-adapter-kysely/test/transactional-adapter-kysely.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-kysely/test/transactional-adapter-kysely.spec.ts @@ -233,3 +233,18 @@ describe('Transactional', () => { }); }); }); + +describe('Default options', () => { + it('Should correctly set default options on the adapter instance', async () => { + const adapter = new TransactionalAdapterKysely({ + kyselyInstanceToken: KYSELY, + defaultTxOptions: { + isolationLevel: 'repeatable read', + }, + }); + + expect(adapter.defaultTxOptions).toEqual({ + isolationLevel: 'repeatable read', + }); + }); +}); diff --git a/packages/transactional-adapters/transactional-adapter-pg-promise/src/lib/transactional-adapter-pg-promise.ts b/packages/transactional-adapters/transactional-adapter-pg-promise/src/lib/transactional-adapter-pg-promise.ts index 7a92fb8b..01f380de 100644 --- a/packages/transactional-adapters/transactional-adapter-pg-promise/src/lib/transactional-adapter-pg-promise.ts +++ b/packages/transactional-adapters/transactional-adapter-pg-promise/src/lib/transactional-adapter-pg-promise.ts @@ -3,7 +3,7 @@ import { IDatabase } from 'pg-promise'; export type Database = IDatabase; -type TxOptions = Parameters[0]; +type PgPromiseTxOptions = Parameters[0]; export interface PgPromiseTransactionalAdapterOptions { /** @@ -15,15 +15,15 @@ export interface PgPromiseTransactionalAdapterOptions { * Default options for the transaction. These will be merged with any transaction-specific options * passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. */ - defaultTxOptions?: TxOptions; + defaultTxOptions?: PgPromiseTxOptions; } export class TransactionalAdapterPgPromise - implements TransactionalAdapter + implements TransactionalAdapter { connectionToken: any; - defaultTxOptions?: TxOptions; + defaultTxOptions?: Partial; constructor(options: PgPromiseTransactionalAdapterOptions) { this.connectionToken = options.dbInstanceToken; @@ -32,17 +32,14 @@ export class TransactionalAdapterPgPromise optionsFactory = (pgPromiseDbInstance: Database) => ({ wrapWithTransaction: async ( - options: TxOptions | null, + options: PgPromiseTxOptions, fn: (...args: any[]) => Promise, setClient: (client?: Database) => void, ) => { - return pgPromiseDbInstance.tx( - { ...this.defaultTxOptions, ...options }, - (tx) => { - setClient(tx as unknown as Database); - return fn(); - }, - ); + return pgPromiseDbInstance.tx(options, (tx) => { + setClient(tx as unknown as Database); + return fn(); + }); }, getFallbackInstance: () => pgPromiseDbInstance, }); diff --git a/packages/transactional-adapters/transactional-adapter-pg-promise/test/transactional-adapter-pg-promise.spec.ts b/packages/transactional-adapters/transactional-adapter-pg-promise/test/transactional-adapter-pg-promise.spec.ts index 5508b239..ca3c49b7 100644 --- a/packages/transactional-adapters/transactional-adapter-pg-promise/test/transactional-adapter-pg-promise.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-pg-promise/test/transactional-adapter-pg-promise.spec.ts @@ -9,7 +9,7 @@ import { Inject, Injectable, Module } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsModule } from 'nestjs-cls'; import { execSync } from 'node:child_process'; -import pgPromise, { txMode } from 'pg-promise'; +import pgPromise from 'pg-promise'; import { Database, TransactionalAdapterPgPromise } from '../src'; type UserRecord = { id: number; name: string; email: string }; @@ -212,93 +212,19 @@ describe('Transactional', () => { ); }); }); - describe('wrapWithTransaction()', () => { - const txMock = jest.fn(); - - beforeEach(() => jest.resetAllMocks()); - - describe('sets the transaction options correctly', () => { - const defaultTxOptions = { tag: 'some-tag' }; - const txOptions = { - mode: new txMode.TransactionMode({ - tiLevel: txMode.isolationLevel.serializable, - }), - }; - - test('no default options, no tx options', async () => { - const transactionalAdapterPgPromise = - new TransactionalAdapterPgPromise({ - dbInstanceToken: 'SOME_TOKEN', - }); - const { wrapWithTransaction } = - transactionalAdapterPgPromise.optionsFactory({ - tx: txMock, - } as unknown as Database); - - await wrapWithTransaction(null, jest.fn(), jest.fn()); - - expect(txMock).toHaveBeenCalledTimes(1); - expect(txMock).toHaveBeenCalledWith({}, expect.anything()); - }); - - test('default options only', async () => { - const transactionalAdapterPgPromise = - new TransactionalAdapterPgPromise({ - dbInstanceToken: 'SOME_TOKEN', - defaultTxOptions, - }); - const { wrapWithTransaction } = - transactionalAdapterPgPromise.optionsFactory({ - tx: txMock, - } as unknown as Database); - - await wrapWithTransaction(null, jest.fn(), jest.fn()); - - expect(txMock).toHaveBeenCalledTimes(1); - expect(txMock).toHaveBeenCalledWith( - defaultTxOptions, - expect.anything(), - ); - }); - - test('tx options only', async () => { - const transactionalAdapterPgPromise = - new TransactionalAdapterPgPromise({ - dbInstanceToken: 'SOME_TOKEN', - }); - const { wrapWithTransaction } = - transactionalAdapterPgPromise.optionsFactory({ - tx: txMock, - } as unknown as Database); - - await wrapWithTransaction(txOptions, jest.fn(), jest.fn()); - - expect(txMock).toHaveBeenCalledTimes(1); - expect(txMock).toHaveBeenCalledWith( - txOptions, - expect.anything(), - ); - }); - - test('default options and tx options', async () => { - const transactionalAdapterPgPromise = - new TransactionalAdapterPgPromise({ - dbInstanceToken: 'SOME_TOKEN', - defaultTxOptions, - }); - const { wrapWithTransaction } = - transactionalAdapterPgPromise.optionsFactory({ - tx: txMock, - } as unknown as Database); +}); - await wrapWithTransaction(txOptions, jest.fn(), jest.fn()); +describe('Default options', () => { + it('Should correctly set default options on the adapter instance', async () => { + const adapter = new TransactionalAdapterPgPromise({ + dbInstanceToken: PG_PROMISE, + defaultTxOptions: { + tag: 'test-tag', + }, + }); - expect(txMock).toHaveBeenCalledTimes(1); - expect(txMock).toHaveBeenCalledWith( - { tag: 'some-tag', mode: txOptions.mode }, - expect.anything(), - ); - }); + expect(adapter.defaultTxOptions).toEqual({ + tag: 'test-tag', }); }); }); diff --git a/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts b/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts index 7de6ddda..bfac6f24 100644 --- a/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts +++ b/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts @@ -13,11 +13,19 @@ export type PrismaTransactionOptions< TClient extends AnyTransactionClient = PrismaClient, > = Parameters[1]; -export interface PrismaTransactionalAdapterOptions { +export interface PrismaTransactionalAdapterOptions< + TClient extends AnyTransactionClient = PrismaClient, +> { /** * The injection token for the PrismaClient instance. */ prismaInjectionToken: any; + + /** + * Default options for the transaction. These will be merged with any transaction-specific options + * passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. + */ + defaultTxOptions?: Partial>; } export class TransactionalAdapterPrisma< @@ -31,8 +39,11 @@ export class TransactionalAdapterPrisma< { connectionToken: any; - constructor(options: { prismaInjectionToken: any }) { + defaultTxOptions?: Partial>; + + constructor(options: PrismaTransactionalAdapterOptions) { this.connectionToken = options.prismaInjectionToken; + this.defaultTxOptions = options.defaultTxOptions; } optionsFactory = (prisma: TClient) => ({ diff --git a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts index c15657ff..32145e7b 100644 --- a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts @@ -67,9 +67,11 @@ class PrismaModule {} plugins: [ new ClsPluginTransactional({ imports: [PrismaModule], - adapter: new TransactionalAdapterPrisma({ - prismaInjectionToken: CUSTOM_PRISMA_CLIENT, - }), + adapter: new TransactionalAdapterPrisma( + { + prismaInjectionToken: CUSTOM_PRISMA_CLIENT, + }, + ), enableTransactionProxy: true, }), ], diff --git a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts index 17ce4c28..1de480ee 100644 --- a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts @@ -161,3 +161,18 @@ describe('Transactional', () => { }); }); }); + +describe('Default options', () => { + it('Should correctly set default options on the adapter instance', async () => { + const adapter = new TransactionalAdapterPrisma({ + prismaInjectionToken: PrismaClient, + defaultTxOptions: { + timeout: 24, + }, + }); + + expect(adapter.defaultTxOptions).toEqual({ + timeout: 24, + }); + }); +}); diff --git a/packages/transactional-adapters/transactional-adapter-typeorm/src/lib/transactional-adapter-typeorm.ts b/packages/transactional-adapters/transactional-adapter-typeorm/src/lib/transactional-adapter-typeorm.ts index abcf3ac3..cd9fcc28 100644 --- a/packages/transactional-adapters/transactional-adapter-typeorm/src/lib/transactional-adapter-typeorm.ts +++ b/packages/transactional-adapters/transactional-adapter-typeorm/src/lib/transactional-adapter-typeorm.ts @@ -7,6 +7,12 @@ export interface TypeOrmTransactionalAdapterOptions { * The injection token for the TypeORM DataSource instance. */ dataSourceToken: any; + + /** + * Default options for the transaction. These will be merged with any transaction-specific options + * passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method. + */ + defaultTxOptions?: Partial; } export interface TypeOrmTransactionOptions { @@ -23,8 +29,11 @@ export class TransactionalAdapterTypeOrm { connectionToken: any; + defaultTxOptions?: Partial; + constructor(options: TypeOrmTransactionalAdapterOptions) { this.connectionToken = options.dataSourceToken; + this.defaultTxOptions = options.defaultTxOptions; } optionsFactory = (dataSource: DataSource) => ({ diff --git a/packages/transactional-adapters/transactional-adapter-typeorm/test/transactional-adapter-typeorm.spec.ts b/packages/transactional-adapters/transactional-adapter-typeorm/test/transactional-adapter-typeorm.spec.ts index e84d9243..e92c4c5f 100644 --- a/packages/transactional-adapters/transactional-adapter-typeorm/test/transactional-adapter-typeorm.spec.ts +++ b/packages/transactional-adapters/transactional-adapter-typeorm/test/transactional-adapter-typeorm.spec.ts @@ -9,7 +9,7 @@ import { Injectable, Module } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsModule } from 'nestjs-cls'; import { execSync } from 'node:child_process'; -import { DataSource, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Column, DataSource, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { TransactionalAdapterTypeOrm } from '../src'; @Entity() @@ -208,3 +208,18 @@ describe('Transactional', () => { }); }); }); + +describe('Default options', () => { + it('Should correctly set default options on the adapter instance', async () => { + const adapter = new TransactionalAdapterTypeOrm({ + dataSourceToken: DataSource, + defaultTxOptions: { + isolationLevel: 'READ COMMITTED', + }, + }); + + expect(adapter.defaultTxOptions).toEqual({ + isolationLevel: 'READ COMMITTED', + }); + }); +}); diff --git a/packages/transactional/src/lib/interfaces.ts b/packages/transactional/src/lib/interfaces.ts index d279ddea..0236688c 100644 --- a/packages/transactional/src/lib/interfaces.ts +++ b/packages/transactional/src/lib/interfaces.ts @@ -11,6 +11,7 @@ export interface MergedTransactionalAdapterOptions extends TransactionalAdapterOptions { connectionName: string | undefined; enableTransactionProxy: boolean; + defaultTxOptions: Partial; } export type TransactionalOptionsAdapterFactory = ( @@ -24,6 +25,11 @@ export interface TransactionalAdapter { */ connectionToken: any; + /** + * Default options for all transactions + */ + defaultTxOptions?: Partial; + /** * Function that accepts the `connection` based on the `connectionToken` * diff --git a/packages/transactional/src/lib/plugin-transactional.ts b/packages/transactional/src/lib/plugin-transactional.ts index 25a033a8..96d475d3 100644 --- a/packages/transactional/src/lib/plugin-transactional.ts +++ b/packages/transactional/src/lib/plugin-transactional.ts @@ -43,6 +43,8 @@ export class ClsPluginTransactional implements ClsPlugin { connectionName: options.connectionName, enableTransactionProxy: options.enableTransactionProxy ?? false, + defaultTxOptions: + options.adapter.defaultTxOptions ?? {}, }; }, }, diff --git a/packages/transactional/src/lib/transaction-host.ts b/packages/transactional/src/lib/transaction-host.ts index 69445e49..5ee8f334 100644 --- a/packages/transactional/src/lib/transaction-host.ts +++ b/packages/transactional/src/lib/transaction-host.ts @@ -131,6 +131,7 @@ export class TransactionHost { fn = firstParam; } propagation ??= Propagation.Required; + options = { ...this._options.defaultTxOptions, ...options }; return this.decidePropagationAndRun(propagation, options, fn); } diff --git a/packages/transactional/test/default-options.spec.ts b/packages/transactional/test/default-options.spec.ts new file mode 100644 index 00000000..d30bdc71 --- /dev/null +++ b/packages/transactional/test/default-options.spec.ts @@ -0,0 +1,152 @@ +import { Injectable, Module } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsModule } from 'nestjs-cls'; +import { ClsPluginTransactional, Transactional, TransactionHost } from '../src'; +import { + MockDbConnection, + TransactionAdapterMock, +} from './transaction-adapter-mock'; + +@Injectable() +class CalledService { + constructor( + private readonly txHost: TransactionHost, + ) {} + + async doWork(num: number) { + return this.txHost.tx.query(`SELECT ${num}`); + } +} + +@Injectable() +class CallingService { + constructor( + private readonly calledService: CalledService, + private readonly txHost: TransactionHost, + ) {} + + @Transactional({ + serializable: true, + }) + async transactionalDecoratorWithDefaultOptions() { + return await this.calledService.doWork(1); + } + + @Transactional({ + serializable: true, + sayHello: false, + }) + async transactionalDecoratorWithCustomOptions() { + return await this.calledService.doWork(2); + } + + async withTransactionWithDefaultOptions() { + return await this.txHost.withTransaction( + { serializable: true }, + async () => this.calledService.doWork(3), + ); + } + + async withTransactionWithCustomOptions() { + return await this.txHost.withTransaction( + { serializable: true, sayHello: false }, + async () => this.calledService.doWork(4), + ); + } +} + +@Module({ + providers: [MockDbConnection], + exports: [MockDbConnection], +}) +class DbConnectionModule {} + +@Module({ + imports: [ + ClsModule.forRoot({ + plugins: [ + new ClsPluginTransactional({ + imports: [DbConnectionModule], + adapter: new TransactionAdapterMock({ + connectionToken: MockDbConnection, + defaultTxOptions: { sayHello: true }, + }), + }), + ], + }), + ], + providers: [CallingService, CalledService], +}) +class AppModule {} + +describe('Using defaultOptions in Transactional adapter', () => { + let module: TestingModule; + let callingService: CallingService; + let mockDbConnection: MockDbConnection; + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + await module.init(); + callingService = module.get(CallingService); + mockDbConnection = module.get(MockDbConnection); + }); + + describe('when using the @Transactional decorator', () => { + it('should merge passed options with default ones', async () => { + const result = + await callingService.transactionalDecoratorWithDefaultOptions(); + expect(result).toEqual({ query: 'SELECT 1' }); + const queries = mockDbConnection.getClientsQueries(); + expect(queries).toEqual([ + [ + '/* Hello */ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;', + 'SELECT 1', + 'COMMIT TRANSACTION;', + ], + ]); + }); + it('should override default options with explicit one', async () => { + const result = + await callingService.transactionalDecoratorWithCustomOptions(); + expect(result).toEqual({ query: 'SELECT 2' }); + const queries = mockDbConnection.getClientsQueries(); + expect(queries).toEqual([ + [ + 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;', + 'SELECT 2', + 'COMMIT TRANSACTION;', + ], + ]); + }); + }); + + describe('when using the withTransaction method', () => { + it('should merge passed options with default ones', async () => { + const result = + await callingService.withTransactionWithDefaultOptions(); + expect(result).toEqual({ query: 'SELECT 3' }); + const queries = mockDbConnection.getClientsQueries(); + expect(queries).toEqual([ + [ + '/* Hello */ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;', + 'SELECT 3', + 'COMMIT TRANSACTION;', + ], + ]); + }); + it('should override default options with explicit one', async () => { + const result = + await callingService.withTransactionWithCustomOptions(); + expect(result).toEqual({ query: 'SELECT 4' }); + const queries = mockDbConnection.getClientsQueries(); + expect(queries).toEqual([ + [ + 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;', + 'SELECT 4', + 'COMMIT TRANSACTION;', + ], + ]); + }); + }); +}); diff --git a/packages/transactional/test/multiple-connections.spec.ts b/packages/transactional/test/multiple-connections.spec.ts index a635e8d8..5a16da56 100644 --- a/packages/transactional/test/multiple-connections.spec.ts +++ b/packages/transactional/test/multiple-connections.spec.ts @@ -128,7 +128,7 @@ class DbConnectionModule2 {} }) class AppModule {} -describe('Transactional', () => { +describe('Transactional - multiple connections', () => { let module: TestingModule; let callingService: CallingService; let mockDbConnection1: MockDbConnection; diff --git a/packages/transactional/test/named-connection.spec.ts b/packages/transactional/test/named-connection.spec.ts index 3548ea5f..ff2aba02 100644 --- a/packages/transactional/test/named-connection.spec.ts +++ b/packages/transactional/test/named-connection.spec.ts @@ -113,7 +113,7 @@ class DbConnectionModule {} }) class AppModule {} -describe('Transactional', () => { +describe('Transactional - named connections', () => { let module: TestingModule; let callingService: CallingService; let mockDbConnection: MockDbConnection; diff --git a/packages/transactional/test/transaction-adapter-mock.ts b/packages/transactional/test/transaction-adapter-mock.ts index 3193fefb..571e7a0b 100644 --- a/packages/transactional/test/transaction-adapter-mock.ts +++ b/packages/transactional/test/transaction-adapter-mock.ts @@ -30,6 +30,7 @@ export class MockDbConnection { export interface MockTransactionOptions { serializable?: boolean; + sayHello?: boolean; } export class TransactionAdapterMock @@ -41,9 +42,16 @@ export class TransactionAdapterMock > { connectionToken: any; - constructor(options: { connectionToken: any }) { + defaultTxOptions: Partial; + + constructor(options: { + connectionToken: any; + defaultTxOptions?: MockTransactionOptions; + }) { this.connectionToken = options.connectionToken; + this.defaultTxOptions = options.defaultTxOptions ?? {}; } + optionsFactory = (connection: MockDbConnection) => ({ wrapWithTransaction: async ( options: MockTransactionOptions | undefined, @@ -58,6 +66,9 @@ export class TransactionAdapterMock 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ' + beginQuery; } + if (options?.sayHello) { + beginQuery = '/* Hello */ ' + beginQuery; + } await client.query(beginQuery); try { const result = await fn();