Skip to content

Commit

Permalink
feat(transactional): enable using lifecycle hooks in TransactionalAda…
Browse files Browse the repository at this point in the history
…pter (#156)
  • Loading branch information
Papooch authored Jun 27, 2024
1 parent ef6768a commit 1ec49ea
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class MyTransactionalAdapterKnex
}

//
optionsFactory = (knexInstance: Knex) => {
optionsFactory(knexInstance: Knex) {
return {
wrapWithTransaction: (
// the options object is the transaction-specific options merged with the default ones
Expand Down Expand Up @@ -169,7 +169,7 @@ export class MyTransactionalAdapterKnex
// highlight-end
getFallbackInstance: () => knexInstance,
};
};
}
}
```

Expand All @@ -194,3 +194,11 @@ ClsModule.forRoot({
When injecting the `TransactionHost`, type it as `TransactionHost<MyTransactionalAdapterKnex>` to get the correct typing of the `tx` property.

In a similar manner, use `@Transactional<MyTransactionalAdapterKnex>()` to get typing for the options object.

:::note

The `TransactionalAdapter` can also implement all [Lifecycle hooks](https://docs.nestjs.com/fundamentals/lifecycle-events) if there's any setup or teardown logic required.

However, being created manually outside of Nest's control, it _can not_ inject any dependencies except for the pre-defined database connection instance via the `connectionToken`.

:::
20 changes: 19 additions & 1 deletion packages/transactional/src/lib/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import {
BeforeApplicationShutdown,
OnApplicationBootstrap,
OnApplicationShutdown,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';

export interface TransactionalAdapterOptions<TTx, TOptions> {
wrapWithTransaction: (
options: TOptions,
Expand All @@ -12,13 +20,23 @@ export interface MergedTransactionalAdapterOptions<TTx, TOptions>
connectionName: string | undefined;
enableTransactionProxy: boolean;
defaultTxOptions: Partial<TOptions>;
onModuleInit?: () => void | Promise<void>;
}

export type TransactionalOptionsAdapterFactory<TConnection, TTx, TOptions> = (
connection: TConnection,
) => TransactionalAdapterOptions<TTx, TOptions>;

export interface TransactionalAdapter<TConnection, TTx, TOptions> {
export type OptionalLifecycleHooks = Partial<
OnModuleInit &
OnModuleDestroy &
OnApplicationBootstrap &
BeforeApplicationShutdown &
OnApplicationShutdown
>;

export interface TransactionalAdapter<TConnection, TTx, TOptions>
extends OptionalLifecycleHooks {
/**
* Token used to inject the `connection` into the adapter.
* It is later used to create transactions.
Expand Down
31 changes: 29 additions & 2 deletions packages/transactional/src/lib/plugin-transactional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ClsModule, ClsPlugin } from 'nestjs-cls';
import { getTransactionToken } from './inject-transaction.decorator';
import {
MergedTransactionalAdapterOptions,
OptionalLifecycleHooks,
TransactionalPluginOptions,
} from './interfaces';
import {
Expand Down Expand Up @@ -36,10 +37,13 @@ export class ClsPluginTransactional implements ClsPlugin {
useFactory: (
connection: any,
): MergedTransactionalAdapterOptions<any, any> => {
const adapterOptions =
options.adapter.optionsFactory(connection);
const adapterOptions = options.adapter.optionsFactory.call(
options.adapter,
connection,
);
return {
...adapterOptions,
...this.bindLifecycleHooks(options),
connectionName: options.connectionName,
enableTransactionProxy:
options.enableTransactionProxy ?? false,
Expand Down Expand Up @@ -70,4 +74,27 @@ export class ClsPluginTransactional implements ClsPlugin {
);
}
}

private bindLifecycleHooks(
options: TransactionalPluginOptions<any, any, any>,
): OptionalLifecycleHooks {
const {
onModuleInit,
onModuleDestroy,
onApplicationBootstrap,
beforeApplicationShutdown,
onApplicationShutdown,
} = options.adapter;
return {
onModuleInit: onModuleInit?.bind(options.adapter),
onModuleDestroy: onModuleDestroy?.bind(options.adapter),
onApplicationBootstrap: onApplicationBootstrap?.bind(
options.adapter,
),
beforeApplicationShutdown: beforeApplicationShutdown?.bind(
options.adapter,
),
onApplicationShutdown: onApplicationShutdown?.bind(options.adapter),
};
}
}
85 changes: 85 additions & 0 deletions packages/transactional/test/lifecycle-hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { ClsModule } from 'nestjs-cls';
import { ClsPluginTransactional } from '../src';
import {
MockDbConnection,
TransactionAdapterMock,
} from './transaction-adapter-mock';

class TransactionalAdapterMockWithHooks extends TransactionAdapterMock {
initCalled = false;
destroyCalled = false;
bootstrapCalled = false;
beforeShutdownCalled = false;
shutdownCalled = false;

onModuleInit() {
this.initCalled = true;
}

onModuleDestroy() {
this.destroyCalled = true;
}

onApplicationBootstrap() {
this.bootstrapCalled = true;
}

beforeApplicationShutdown() {
this.beforeShutdownCalled = true;
}

onApplicationShutdown() {
this.shutdownCalled = true;
}
}

describe('Lifecycle hooks', () => {
it('should trigger all lifecycle hooks defined in the TransactionalAdapter', async () => {
@Module({
providers: [MockDbConnection],
exports: [MockDbConnection],
})
class DbConnectionModule {}

const adapter = new TransactionalAdapterMockWithHooks({
connectionToken: MockDbConnection,
});

@Module({
imports: [
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [DbConnectionModule],
adapter: adapter,
}),
],
}),
],
})
class AppModule {}

const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();

expect(adapter.bootstrapCalled).toBe(false);
expect(adapter.initCalled).toBe(false);

await module.init();

expect(adapter.bootstrapCalled).toBe(true);
expect(adapter.initCalled).toBe(true);
expect(adapter.beforeShutdownCalled).toBe(false);
expect(adapter.shutdownCalled).toBe(false);
expect(adapter.destroyCalled).toBe(false);

await module.close();

expect(adapter.beforeShutdownCalled).toBe(true);
expect(adapter.shutdownCalled).toBe(true);
expect(adapter.destroyCalled).toBe(true);
});
});

0 comments on commit 1ec49ea

Please sign in to comment.