-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from pnkp/feat/multiDatabaseConnections
Feat/multi database connections
- Loading branch information
Showing
19 changed files
with
515 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
133
libs/database-session/test/database-session.module.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.