Skip to content

Commit

Permalink
Merge pull request #5 from pnkp/feat/multiDatabaseConnections
Browse files Browse the repository at this point in the history
Feat/multi database connections
  • Loading branch information
pnkp authored Mar 6, 2021
2 parents 60161d6 + 63dd690 commit 02af9b5
Show file tree
Hide file tree
Showing 19 changed files with 515 additions and 211 deletions.
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# About package
This package able to manager easily way to database transaction, now you can start and commit transaction wherever you are.
_**WARNING: Package running in REQUEST SCOPE**_

## Installation
```bash
npm install --save @antyper/database-session typeorm @nestjs/common rxjs
# you have to install a database driver for e.g:
npm install --save pg
```

## Configuration

```typescript
@Module({
providers: [],
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
password: 'postgres',
username: 'postgres',
synchronize: true,
entities: [ExampleModel],
}),
DatabaseSessionModule.forRoot(),
],
controllers: [],
})
export class AppModule {}
```

## Use case
```typescript
@Injectable()
export class ExampleRepository {
private databaseSession: DatabaseSession;
constructor(
@InjectDatabaseSessionManager()
private readonly databaseSessionManager: DatabaseSessionManager,
) {
this.databaseSession = this.databaseSessionManager.getDatabaseSession();
}

async save(exampleModel: Partial<ExampleModel>): Promise<ExampleModel> {
const repository = this.databaseSession.getRepository(ExampleModel);
return await repository.save(exampleModel);
}
}

@Controller('transactions')
export class TransactionController {
private readonly databaseSession: DatabaseSession;
constructor(
private readonly exampleRepository: ExampleRepository,
@InjectDatabaseSessionManager()
private readonly databaseSessionManager: DatabaseSessionManager,
) {
this.databaseSession = this.databaseSessionManager.getDatabaseSession();
}

@Post()
async commitTransaction(
@Body() data: { value: string },
): Promise<ExampleModel> {
try {
// starting transacrtion
await this.databaseSession.transactionStart();
const result = await this.exampleRepository.save(data);

// commiting transaction
await this.databaseSession.transactionCommit();
return result;
} catch (e) {

// rollback transaction
await this.databaseSession.transactionRollback();
throw e;
}
}
}
```

```typescript
// getting DatabaseSession for "default" connection
const databaseSession = this.databaseSessionManager.getDatabaseSession();

const connectionName = "secondDatabaseConnectionName";
const databaseSessionSecondDatabase = this.databaseSessionManager.getDatabaseSession(
connectionName,
);
```
9 changes: 8 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ services:
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
- 5432:5432

db2:
image: library/postgres:13.2-alpine
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5433:5432
26 changes: 26 additions & 0 deletions libs/database-session/src/database-session.manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ConnectionManager } from 'typeorm';
import { DatabaseSession } from './database-session';
import { composeDatabaseSessionProviderName } from './inject-decorators';
import { TypeOrmDatabaseSession } from './type-orm.database-session';

export class DatabaseSessionManager {
private databaseSessions: Map<string, DatabaseSession> = new Map<
string,
DatabaseSession
>();

constructor(connectionManager: ConnectionManager) {
connectionManager.connections.forEach((connection) => {
this.databaseSessions.set(
composeDatabaseSessionProviderName(connection.name),
new TypeOrmDatabaseSession(connection),
);
});
}

getDatabaseSession(connectionName?: string): DatabaseSession {
return this.databaseSessions.get(
composeDatabaseSessionProviderName(connectionName),
);
}
}
82 changes: 48 additions & 34 deletions libs/database-session/src/database-session.module.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,62 @@
import { DynamicModule, Global, Module, Scope } from '@nestjs/common';
import { TypeOrmDatabaseSession } from './type-orm.database-session';
import { DATABASE_SESSION, SESSION_QUERY_RUNNER } from './inject-decorators';
import { Connection } from 'typeorm';
import { FactoryProvider } from '@nestjs/common/interfaces/modules/provider.interface';
import {
DynamicModule,
FactoryProvider,
Global,
Module,
Scope,
} from '@nestjs/common';
import { DATABASE_SESSION_MANAGER } from './inject-decorators';
import { ConnectionManager, getConnectionManager } from 'typeorm';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { DatabaseSessionManager } from './database-session.manager';
import { Provider } from '@nestjs/common/interfaces/modules/provider.interface';

@Global()
@Module({})
export class DatabaseSessionModule {
static forRootAsync(factory: DatabaseSessionModuleOptions): DynamicModule {
return DatabaseSessionModule.forRoot(factory);
private static readonly DATABASE_SESSION_OPTIONS_PROVIDER =
'DATABASE_SESSION_OPTIONS_PROVIDER';

static async forRoot(): Promise<DynamicModule> {
return this.forRootAsync();
}

private static forRoot(factory: DatabaseSessionModuleOptions): DynamicModule {
return {
providers: [
{
useFactory: factory.useFactory,
inject: factory.inject,
provide: 'DatabaseSessionOptions',
},
{
provide: DATABASE_SESSION,
useFactory: async (connection: Connection) => {
return new TypeOrmDatabaseSession(connection);
},
scope: Scope.REQUEST,
inject: ['DatabaseSessionOptions'],
static forRootAsync(options?: DatabaseSessionModuleOptions) {
const providers: Provider[] = [
{
provide: DATABASE_SESSION_MANAGER,
useFactory: (connectionManager?: ConnectionManager) => {
connectionManager = connectionManager ?? getConnectionManager();
return new DatabaseSessionManager(connectionManager);
},
{
provide: SESSION_QUERY_RUNNER,
useFactory: (typeOrmDatabaseSession: TypeOrmDatabaseSession) => {
return typeOrmDatabaseSession.getQueryRunner();
},
inject: [DATABASE_SESSION],
},
],
exports: [DATABASE_SESSION, SESSION_QUERY_RUNNER],
imports: factory.imports,
scope: Scope.REQUEST,
inject: options?.inject ?? [],
},
];
if (options) {
providers.push({
provide: this.DATABASE_SESSION_OPTIONS_PROVIDER,
useFactory: options.useFactory,
inject: options.inject,
});
}

return {
providers,
exports: [DATABASE_SESSION_MANAGER],
module: DatabaseSessionModule,
imports: options?.imports ?? [],
};
}
}

export interface DatabaseSessionModuleOptions
extends Omit<FactoryProvider<Promise<Connection>>, 'provide' | 'scope'> {
imports?: any[];
extends Omit<
FactoryProvider<Promise<ConnectionManager>>,
'provide' | 'scope'
> {
imports?: Array<
Type | DynamicModule | Promise<DynamicModule> | ForwardReference
>;
}
18 changes: 13 additions & 5 deletions libs/database-session/src/inject-decorators.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { Inject } from '@nestjs/common';

export const DATABASE_SESSION = 'DatabaseSession';
export const SESSION_QUERY_RUNNER = 'SessionQueryRunner';
export const DATABASE_SESSION_MANAGER = 'DatabaseSessionManager';

export const InjectDatabaseSession: () => ParameterDecorator = () =>
Inject(DATABASE_SESSION);
export const composeDatabaseSessionProviderName = (
connectionName = 'default',
) => {
return `${DATABASE_SESSION}_connection_${connectionName}`;
};

export const InjectSessionQueryRunner: () => ParameterDecorator = () =>
Inject(SESSION_QUERY_RUNNER);
export const InjectDatabaseSession: (
connectionName?: string,
) => ParameterDecorator = (connectionName?: string) =>
Inject(composeDatabaseSessionProviderName(connectionName));

export const InjectDatabaseSessionManager = () =>
Inject(DATABASE_SESSION_MANAGER);
133 changes: 133 additions & 0 deletions libs/database-session/test/database-session.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { Connection } from 'typeorm';
import { getConnectionToken } from '@nestjs/typeorm';
import {
DatabaseSessionTestModule,
SECOND_DATABASE_CONNECTION,
} from './module/database-session-test.module';
import { ExampleModel } from './module/example.model';

describe('DatabaseSessionModule', () => {
let app: INestApplication;
let connection: Connection;
let connectionSecondDatabase: Connection;

beforeAll(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
imports: [DatabaseSessionTestModule],
}).compile();

app = moduleRef.createNestApplication();
connection = app.get(getConnectionToken());
connectionSecondDatabase = app.get(
getConnectionToken(SECOND_DATABASE_CONNECTION),
);
await app.init();
await clearExampleTable(connection);
await clearExampleTable(connectionSecondDatabase);
});

afterEach(async () => {
await clearExampleTable(connection);
await clearExampleTable(connectionSecondDatabase);
});

const clearExampleTable = async (connection: Connection) => {
await connection.query('delete from example_model;');
await connection.query('alter sequence example_model_id_seq restart 1');
};

const getLastRow = async (connection: Connection): Promise<ExampleModel> => {
return await connection
.getRepository<ExampleModel>(ExampleModel)
.findOne({ order: { id: 'DESC' } });
};

const getRows = async (connection: Connection): Promise<ExampleModel[]> => {
return await connection.getRepository<ExampleModel>(ExampleModel).find();
};

describe('one database session', () => {
it(`should commit transaction`, async () => {
const response = await request(app.getHttpServer())
.post('/transactions')
.send({ value: 'test value' });

const lastRow: ExampleModel = await getLastRow(connection);

expect(response.status).toBe(201);
expect(lastRow).toMatchObject({ id: 1, value: 'test value' });
});

it(`should rollback transaction`, async () => {
const result = await request(app.getHttpServer())
.delete('/transactions')
.send({ value: 'test value' });

const rows: ExampleModel[] = await getRows(connection);

expect(result.status).toBe(500);
expect(rows.length).toBe(1);
expect(rows[0]).toMatchObject({
id: 2,
value: 'rollback transaction',
});
expect(rows[1]).toBeUndefined();
});
});

describe('two database sessions', () => {
it(`should commit transaction`, async () => {
const response = await request(app.getHttpServer())
.post('/transactions/second-database')
.send({ value: 'second-database' });

const lastRow: ExampleModel = await getLastRow(connectionSecondDatabase);

expect(response.status).toBe(201);
expect(lastRow).toMatchObject({ id: 1, value: 'second-database' });
});

it(`should rollback transaction`, async () => {
const result = await request(app.getHttpServer())
.delete('/transactions/second-database')
.send({ value: 'second-database' });

const rows: ExampleModel[] = await getRows(connectionSecondDatabase);

expect(result.status).toBe(500);
expect(rows.length).toBe(1);
expect(rows[0]).toMatchObject({
id: 2,
value: 'rollback transaction in second database',
});
expect(rows[1]).toBeUndefined();
});
});

describe('combine of two database transaction', () => {
it('should commit database transaction in default database connection and rollback transaction in second database', async () => {
await request(app.getHttpServer())
.post('/transactions/combine')
.send({ value: 'default database' });

const lastRowFromSecondDatabase: ExampleModel = await getLastRow(
connectionSecondDatabase,
);
const lastRowFromDefaultDatabase: ExampleModel = await getLastRow(
connection,
);

expect(lastRowFromSecondDatabase).toMatchObject({
id: 2,
value: 'rollback transaction in second database',
});
expect(lastRowFromDefaultDatabase).toMatchObject({
id: 1,
value: 'default database',
});
});
});
});
Loading

0 comments on commit 02af9b5

Please sign in to comment.