Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transactional): add default options parameter to transactional adapter #145

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A transactional adapter is an instance of an object implementing the following i
```ts
interface TransactionalAdapter<TConnection, TTx, TOptions> {
connectionToken: any;
defaultTyOptions?: Partial<TOptions>;
optionsFactory: TransactionalOptionsAdapterFactory<
TConnection,
TTx,
Expand All @@ -19,7 +20,9 @@ interface TransactionalAdapter<TConnection, TTx, TOptions> {

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<TTx, TOptions> {
Expand All @@ -35,7 +38,7 @@ interface TransactionalAdapterOptions<TTx, TOptions> {
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.

Expand Down Expand Up @@ -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<Knex.TransactionConfig>;

// 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<Knex.TransactionConfig>,
) {
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<any>,
setTx: (client: Knex) => void,
Expand Down Expand Up @@ -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
}),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ 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<Knex.TransactionConfig>;
}

export class TransactionalAdapterKnex
implements TransactionalAdapter<Knex, Knex, Knex.TransactionConfig>
{
connectionToken: any;

defaultTxOptions?: Partial<Knex.TransactionConfig>;

constructor(options: KnexTransactionalAdapterOptions) {
this.connectionToken = options.knexInstanceToken;
this.defaultTxOptions = options.defaultTxOptions;
}

optionsFactory = (knexInstance: Knex) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<KyselyTransactionOptions>;
}

export interface KyselyTransactionOptions {
Expand All @@ -15,12 +21,16 @@ export interface KyselyTransactionOptions {
}

export class TransactionalAdapterKysely<DB = any>
implements TransactionalAdapter<Kysely<DB>, Kysely<DB>, any>
implements
TransactionalAdapter<Kysely<DB>, Kysely<DB>, KyselyTransactionOptions>
{
connectionToken: any;

defaultTxOptions?: Partial<KyselyTransactionOptions>;

constructor(options: KyselyTransactionalAdapterOptions) {
this.connectionToken = options.kyselyInstanceToken;
this.defaultTxOptions = options.defaultTxOptions;
}

optionsFactory = (kyselyDb: Kysely<DB>) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IDatabase } from 'pg-promise';

export type Database = IDatabase<unknown>;

type TxOptions = Parameters<Database['tx']>[0];
type PgPromiseTxOptions = Parameters<Database['tx']>[0];

export interface PgPromiseTransactionalAdapterOptions {
/**
Expand All @@ -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<Database, Database, any>
implements TransactionalAdapter<Database, Database, PgPromiseTxOptions>
{
connectionToken: any;

defaultTxOptions?: TxOptions;
defaultTxOptions?: Partial<PgPromiseTxOptions>;

constructor(options: PgPromiseTransactionalAdapterOptions) {
this.connectionToken = options.dbInstanceToken;
Expand All @@ -32,17 +32,14 @@ export class TransactionalAdapterPgPromise

optionsFactory = (pgPromiseDbInstance: Database) => ({
wrapWithTransaction: async (
options: TxOptions | null,
options: PgPromiseTxOptions,
fn: (...args: any[]) => Promise<any>,
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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ export type PrismaTransactionOptions<
TClient extends AnyTransactionClient = PrismaClient,
> = Parameters<TClient['$transaction']>[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<PrismaTransactionOptions<TClient>>;
}

export class TransactionalAdapterPrisma<
Expand All @@ -31,8 +39,11 @@ export class TransactionalAdapterPrisma<
{
connectionToken: any;

constructor(options: { prismaInjectionToken: any }) {
defaultTxOptions?: Partial<PrismaTransactionOptions<TClient>>;

constructor(options: PrismaTransactionalAdapterOptions<TClient>) {
this.connectionToken = options.prismaInjectionToken;
this.defaultTxOptions = options.defaultTxOptions;
}

optionsFactory = (prisma: TClient) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ class PrismaModule {}
plugins: [
new ClsPluginTransactional({
imports: [PrismaModule],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: CUSTOM_PRISMA_CLIENT,
}),
adapter: new TransactionalAdapterPrisma<CustomPrismaClient>(
{
prismaInjectionToken: CUSTOM_PRISMA_CLIENT,
},
),
enableTransactionProxy: true,
}),
],
Expand Down
Loading
Loading