Skip to content

Commit

Permalink
feat: support multiple database connections (#56)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Adámek <banan23@gmail.com>
  • Loading branch information
tsangste and B4nan committed Feb 20, 2022
1 parent 1e7eae4 commit df4725b
Show file tree
Hide file tree
Showing 15 changed files with 1,214 additions and 175 deletions.
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export class MyService {

## Using `AsyncLocalStorage` for request context

> Since v5 AsyncLocalStorage is used inside RequestContext helper so this section is no longer valid.
By default, the `domain` api is used in the `RequestContext` helper. Since `@mikro-orm/core@4.0.3`,
you can use the new `AsyncLocalStorage` too, if you are on up to date node version:

Expand Down Expand Up @@ -208,8 +210,7 @@ app.use((req, res, next) => {

## Using NestJS `Injection Scopes` for request context

By default, the `domain` api is used in the `RequestContext` helper. Since `@nestjs/common@6`,
you can use the new `Injection Scopes` (https://docs.nestjs.com/fundamentals/injection-scopes) too:
Since `@nestjs/common@6`, you can use the new `Injection Scopes` (https://docs.nestjs.com/fundamentals/injection-scopes) too:

```typescript
import { Scope } from '@nestjs/common';
Expand Down Expand Up @@ -317,6 +318,73 @@ async function bootstrap() {

More information about [enableShutdownHooks](https://docs.nestjs.com/fundamentals/lifecycle-events#application-shutdown)

## Multiple Database Connections

You can define multiple database connections by registering multiple `MikroOrmModule` and setting their `contextName`. If you want to use middleware request context you must disable automatic middleware and register `MikroOrmModule` with `forMiddleware()` or use NestJS `Injection Scope`

```typescript
@Module({
imports: [
MikroOrmModule.forRoot({
contextName: 'db1',
registerRequestContext: false, // disable automatatic middleware
...
}),
MikroOrmModule.forRoot({
contextName: 'db2',
registerRequestContext: false, // disable automatatic middleware
...
}),
MikroOrmModule.forMiddleware()
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```

To access different `MikroORM`/`EntityManager` connections you have to use the new injection tokens `@InjectMikroORM()`/`@InjectEntityManager()` where you are required to pass the `contextName` in:

```ts
@Injectable()
export class MyService {

constructor(@InjectMikroORM('db1') private readonly orm1: MikroORM,
@InjectMikroORM('db2') private readonly orm2: MikroORM,
@InjectEntityManager('db1') private readonly em1: EntityManager,
@InjectEntityManager('db2') private readonly em2: EntityManager) { }

}
```

When defining your repositories with `forFeature()` method you will need to set which `contextName` you want it registered against:

```typescript
// photo.module.ts

@Module({
imports: [MikroOrmModule.forFeature([Photo], 'db1')],
providers: [PhotoService],
controllers: [PhotoController],
})
export class PhotoModule {}
```

When using the `@InjectRepository` decorator you will also need to pass the `contextName` you want to get it from:

```typescript
@Injectable()
export class PhotoService {
constructor(
@InjectRepository(Photo, 'db1')
private readonly photoRepository: EntityRepository<Photo>
) {}

// ...

}
```

## Testing

The `nestjs-mikro-orm` package exposes `getRepositoryToken()` function that returns prepared token based on a given entity to allow mocking the repository.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,19 @@
"@mikro-orm/sqlite": "^5.0.1",
"@nestjs/common": "^8.2.6",
"@nestjs/core": "^8.2.6",
"@nestjs/platform-express": "^8.2.6",
"@nestjs/testing": "^8.2.6",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.17",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "~5.12.0",
"@typescript-eslint/parser": "~5.12.0",
"conventional-changelog": "^3.1.25",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^8.9.0",
"jest": "^27.5.1",
"rxjs": "^7.5.4",
"supertest": "^6.2.2",
"ts-jest": "^27.1.3",
"ts-node": "^10.5.0",
"typescript": "4.5.5"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './mikro-orm.module';
export * from './mikro-orm.common';
export * from './mikro-orm.middleware';
export * from './multiple-mikro-orm.middleware';
export * from './typings';
15 changes: 15 additions & 0 deletions src/middleware.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MikroOrmMiddlewareModuleOptions, NestMiddlewareConsumer } from './typings';
import type { MiddlewareConsumer } from '@nestjs/common';

export function forRoutesPath(options: MikroOrmMiddlewareModuleOptions, consumer: MiddlewareConsumer) {
const isNestMiddleware = (consumer: MiddlewareConsumer): consumer is NestMiddlewareConsumer => {
return typeof (consumer as any).httpAdapter === 'object';
};

const usingFastify = (consumer: NestMiddlewareConsumer) => {
return consumer.httpAdapter.constructor.name.toLowerCase().startsWith('fastify');
};

return options.forRoutesPath ??
(isNestMiddleware(consumer) && usingFastify(consumer) ? '(.*)' : '*');
}
81 changes: 48 additions & 33 deletions src/mikro-orm-core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import type { DynamicModule, MiddlewareConsumer, OnApplicationShutdown, Type } f
import { Global, Inject, Module, RequestMethod } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

import { MIKRO_ORM_MODULE_OPTIONS } from './mikro-orm.common';
import { MikroOrmMiddleware } from './mikro-orm.middleware';
import {
CONTEXT_NAMES,
getEntityManagerToken,
getMikroORMToken,
getMongoEntityManagerToken,
getSqlEntityManagerToken,
MIKRO_ORM_MODULE_OPTIONS,
} from './mikro-orm.common';
import { createAsyncProviders, createMikroOrmEntityManagerProvider, createMikroOrmProvider } from './mikro-orm.providers';
import type { MikroOrmModuleAsyncOptions, MikroOrmModuleSyncOptions, NestMiddlewareConsumer } from './typings';
import type { MikroOrmModuleAsyncOptions, MikroOrmModuleSyncOptions } from './typings';
import { MikroOrmModuleOptions } from './typings';
import { MikroOrmMiddleware } from './mikro-orm.middleware';
import { forRoutesPath } from './middleware.helper';

enum EntityManagerModuleName {
Knex = '@mikro-orm/knex',
Expand Down Expand Up @@ -49,73 +57,80 @@ export class MikroOrmCoreModule implements OnApplicationShutdown {
private readonly moduleRef: ModuleRef) { }

static async forRoot(options?: MikroOrmModuleSyncOptions): Promise<DynamicModule> {
const contextName = this.setContextName(options?.contextName);
return {
module: MikroOrmCoreModule,
providers: [
{ provide: MIKRO_ORM_MODULE_OPTIONS, useValue: options || {} },
createMikroOrmProvider(),
createMikroOrmEntityManagerProvider(options?.scope),
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, SqlEntityManager))),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, MongoEntityManager))),
createMikroOrmProvider(contextName),
createMikroOrmEntityManagerProvider(options?.scope, EntityManager, contextName),
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, contextName ? getSqlEntityManagerToken(contextName) : SqlEntityManager, contextName))),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, contextName ? getMongoEntityManagerToken(contextName) : MongoEntityManager, contextName))),
],
exports: [
MikroORM,
EntityManager,
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => SqlEntityManager)),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => MongoEntityManager)),
contextName ? getMikroORMToken(contextName) : MikroORM,
contextName ? getEntityManagerToken(contextName) : EntityManager,
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => contextName ? getSqlEntityManagerToken(contextName) : SqlEntityManager)),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => contextName ? getMongoEntityManagerToken(contextName) : MongoEntityManager)),
],
};
}

static async forRootAsync(options: MikroOrmModuleAsyncOptions): Promise<DynamicModule> {
const contextName = this.setContextName(options?.contextName);
return {
module: MikroOrmCoreModule,
imports: options.imports || [],
providers: [
...(options.providers || []),
...createAsyncProviders(options),
createMikroOrmProvider(),
createMikroOrmEntityManagerProvider(options.scope),
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => createMikroOrmEntityManagerProvider(options.scope, SqlEntityManager))),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => createMikroOrmEntityManagerProvider(options.scope, MongoEntityManager))),
...createAsyncProviders({ ...options, contextName: options.contextName }),
createMikroOrmProvider(contextName),
createMikroOrmEntityManagerProvider(options.scope, EntityManager, contextName),
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, contextName ? getSqlEntityManagerToken(contextName) : SqlEntityManager, contextName))),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => createMikroOrmEntityManagerProvider(options?.scope, contextName ? getMongoEntityManagerToken(contextName) : MongoEntityManager, contextName))),
],
exports: [
MikroORM,
EntityManager,
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => SqlEntityManager)),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => MongoEntityManager)),
contextName ? getMikroORMToken(contextName) : MikroORM,
contextName ? getEntityManagerToken(contextName) : EntityManager,
...(await whenModuleAvailable(EntityManagerModuleName.Knex, ({ SqlEntityManager }) => contextName ? getSqlEntityManagerToken(contextName) : SqlEntityManager)),
...(await whenModuleAvailable(EntityManagerModuleName.MongoDb, ({ MongoEntityManager }) => contextName ? getMongoEntityManagerToken(contextName) : MongoEntityManager)),
],
};
}

async onApplicationShutdown() {
const orm = this.moduleRef.get(MikroORM);
const token = this.options.contextName ? getMikroORMToken(this.options.contextName) : MikroORM;
const orm = this.moduleRef.get(token);

if (orm) {
await orm.close();
}

CONTEXT_NAMES.length = 0;
}

configure(consumer: MiddlewareConsumer): void {
if (this.options.registerRequestContext === false) {
return;
}

const isNestMiddleware = (consumer: MiddlewareConsumer): consumer is NestMiddlewareConsumer => {
return typeof (consumer as any).httpAdapter === 'object';
};
consumer
.apply(MikroOrmMiddleware) // register request context automatically
.forRoutes({ path: forRoutesPath(this.options, consumer), method: RequestMethod.ALL });
}

const usingFastify = (consumer: NestMiddlewareConsumer) => {
return consumer.httpAdapter.constructor.name.toLowerCase().startsWith('fastify');
};
private static setContextName(contextName?: string) {
if (!contextName) {
return;
}

const forRoutesPath =
this.options.forRoutesPath ??
(isNestMiddleware(consumer) && usingFastify(consumer) ? '(.*)' : '*');
if (CONTEXT_NAMES.includes(contextName)) {
throw new Error(`ContextName '${contextName}' already registered`);
}

consumer
.apply(MikroOrmMiddleware) // register request context automatically
.forRoutes({ path: forRoutesPath, method: RequestMethod.ALL });
CONTEXT_NAMES.push(contextName);

return contextName;
}

}
41 changes: 41 additions & 0 deletions src/mikro-orm-middleware.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { MiddlewareConsumer } from '@nestjs/common';
import { Global, Inject, Module, RequestMethod } from '@nestjs/common';

import { CONTEXT_NAMES, getMikroORMToken, MIKRO_ORM_MODULE_OPTIONS } from './mikro-orm.common';
import { MultipleMikroOrmMiddleware } from './multiple-mikro-orm.middleware';
import { MikroOrmMiddlewareModuleOptions } from './typings';
import type { MikroORM } from '@mikro-orm/core';
import { forRoutesPath } from './middleware.helper';

@Global()
@Module({})
export class MikroOrmMiddlewareModule {

constructor(@Inject(MIKRO_ORM_MODULE_OPTIONS)
private readonly options: MikroOrmMiddlewareModuleOptions) { }

static forMiddleware(options?: MikroOrmMiddlewareModuleOptions) {
// Work around due to nestjs not supporting the ability to register multiple types
// https://github.com/nestjs/nest/issues/770
// https://github.com/nestjs/nest/issues/4786#issuecomment-755032258 - workaround suggestion
const inject = CONTEXT_NAMES.map(name => getMikroORMToken(name));
return {
module: MikroOrmMiddlewareModule,
providers: [
{ provide: MIKRO_ORM_MODULE_OPTIONS, useValue: options || {} },
{
provide: 'MikroORMs',
useFactory: (...args: MikroORM[]) => args,
inject,
},
],
};
}

configure(consumer: MiddlewareConsumer): void {
consumer
.apply(MultipleMikroOrmMiddleware)
.forRoutes({ path: forRoutesPath(this.options, consumer), method: RequestMethod.ALL });
}

}
19 changes: 17 additions & 2 deletions src/mikro-orm.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { Inject, Logger } from '@nestjs/common';

export const MIKRO_ORM_MODULE_OPTIONS = Symbol('mikro-orm-module-options');
export const REGISTERED_ENTITIES = new Set<EntityName<AnyEntity>>();
export const CONTEXT_NAMES: string[] = [];
export const logger = new Logger(MikroORM.name);
export const getRepositoryToken = <T> (entity: EntityName<T>) => `${Utils.className(entity)}Repository`;
export const InjectRepository = <T> (entity: EntityName<T>) => Inject(getRepositoryToken(entity));

export const getMikroORMToken = (name: string) => `${name}_MikroORM`;
export const InjectMikroORM = (name: string) => Inject(getMikroORMToken(name));

export const getEntityManagerToken = (name: string) => `${name}_EntityManager`;
export const InjectEntityManager = (name: string) => Inject(getEntityManagerToken(name));
export const getSqlEntityManagerToken = (name: string) => `${name}_SqlEntityManager`;
export const InjectSqlEntityManager = (name: string) => Inject(getSqlEntityManagerToken(name));
export const getMongoEntityManagerToken = (name: string) => `${name}_MongoEntityManager`;
export const InjectMongoEntityManager = (name: string) => Inject(getMongoEntityManagerToken(name));

export const getRepositoryToken = <T> (entity: EntityName<T>, name?: string) => {
const suffix = name ? `_${name}` : '';
return `${Utils.className(entity)}Repository${suffix}`;
};
export const InjectRepository = <T> (entity: EntityName<T>, name?: string) => Inject(getRepositoryToken(entity, name));
14 changes: 11 additions & 3 deletions src/mikro-orm.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { DynamicModule } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { createMikroOrmRepositoryProviders } from './mikro-orm.providers';
import { MikroOrmCoreModule } from './mikro-orm-core.module';
import type { MikroOrmModuleAsyncOptions, MikroOrmModuleSyncOptions } from './typings';
import type { MikroOrmModuleAsyncOptions, MikroOrmModuleSyncOptions, MikroOrmMiddlewareModuleOptions } from './typings';
import { REGISTERED_ENTITIES } from './mikro-orm.common';
import { MikroOrmMiddlewareModule } from './mikro-orm-middleware.module';

@Module({})
export class MikroOrmModule {
Expand All @@ -24,9 +25,9 @@ export class MikroOrmModule {
};
}

static forFeature(options: EntityName<AnyEntity>[] | { entities?: EntityName<AnyEntity>[] }): DynamicModule {
static forFeature(options: EntityName<AnyEntity>[] | { entities?: EntityName<AnyEntity>[] }, contextName?: string): DynamicModule {
const entities = Array.isArray(options) ? options : (options.entities || []);
const providers = createMikroOrmRepositoryProviders(entities);
const providers = createMikroOrmRepositoryProviders(entities, contextName);

for (const e of entities) {
if (!Utils.isString(e)) {
Expand All @@ -41,4 +42,11 @@ export class MikroOrmModule {
};
}

static forMiddleware(options?: MikroOrmMiddlewareModuleOptions): DynamicModule {
return {
module: MikroOrmModule,
imports: [MikroOrmMiddlewareModule.forMiddleware(options)],
};
}

}
Loading

0 comments on commit df4725b

Please sign in to comment.