Skip to content

Commit

Permalink
feat(core): allow setting a custom scope for the EntityManager provid…
Browse files Browse the repository at this point in the history
…er (#9)

Closes #2
  • Loading branch information
jsprw committed Sep 25, 2020
1 parent f97424a commit c11e0ea
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 18 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class MyService {

## Using `AsyncLocalStorage` for request context

By default, `domain` api use used in the `RequestContext` helper. Since `@mikro-orm/core@4.0.3`,
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:

```typescript
Expand Down Expand Up @@ -206,6 +206,52 @@ 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:

```typescript
import { Scope } from '@nestjs/common';

@Module({
imports: [
MikroOrmModule.forRoot({
// ...
registerRequestContext: false, // disable automatatic middleware
scope: Scope.REQUEST
}),
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
```

Or, if you're using the Async provider:
```typescript
import { Scope } from '@nestjs/common';

@Module({
imports: [
MikroOrmModule.forRootAsync({
// ...
useFactory: () => ({
// ...
registerRequestContext: false, // disable automatatic middleware
}),
scope: Scope.REQUEST
})
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
```

> Please note that this might have some impact on performance,
> see: https://docs.nestjs.com/fundamentals/injection-scopes#performance
## Using custom repositories

When using custom repositories, we can get around the need for `@InjectRepository()`
Expand Down
16 changes: 8 additions & 8 deletions src/mikro-orm-core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ModuleRef } from '@nestjs/core';
import { MIKRO_ORM_MODULE_OPTIONS } from './mikro-orm.common';
import { MikroOrmMiddleware } from './mikro-orm.middleware';
import { createAsyncProviders, createMikroOrmEntityManagerProvider, createMikroOrmProvider } from './mikro-orm.providers';
import { MikroOrmModuleAsyncOptions, MikroOrmModuleOptions, NestMiddlewareConsumer } from './typings';
import { MikroOrmModuleAsyncOptions, MikroOrmModuleOptions, MikroOrmModuleSyncOptions, NestMiddlewareConsumer } from './typings';

@Global()
@Module({})
Expand All @@ -15,15 +15,15 @@ export class MikroOrmCoreModule implements OnApplicationShutdown {
private readonly options: MikroOrmModuleOptions,
private readonly moduleRef: ModuleRef) { }

static forRoot(options?: Options): DynamicModule {
static forRoot(options?: MikroOrmModuleSyncOptions): DynamicModule {
return {
module: MikroOrmCoreModule,
providers: [
{ provide: MIKRO_ORM_MODULE_OPTIONS, useValue: options || {} },
createMikroOrmProvider(),
createMikroOrmEntityManagerProvider(),
createMikroOrmEntityManagerProvider('SqlEntityManager'),
createMikroOrmEntityManagerProvider('MongoEntityManager'),
createMikroOrmEntityManagerProvider(options?.scope),
createMikroOrmEntityManagerProvider(options?.scope, 'SqlEntityManager'),
createMikroOrmEntityManagerProvider(options?.scope, 'MongoEntityManager'),
],
exports: [MikroORM, EntityManager, 'SqlEntityManager', 'MongoEntityManager'],
};
Expand All @@ -37,9 +37,9 @@ export class MikroOrmCoreModule implements OnApplicationShutdown {
...(options.providers || []),
...createAsyncProviders(options),
createMikroOrmProvider(),
createMikroOrmEntityManagerProvider(),
createMikroOrmEntityManagerProvider('SqlEntityManager'),
createMikroOrmEntityManagerProvider('MongoEntityManager'),
createMikroOrmEntityManagerProvider(options.scope),
createMikroOrmEntityManagerProvider(options.scope, 'SqlEntityManager'),
createMikroOrmEntityManagerProvider(options.scope, 'MongoEntityManager'),
],
exports: [MikroORM, EntityManager, 'SqlEntityManager', 'MongoEntityManager'],
};
Expand Down
4 changes: 2 additions & 2 deletions src/mikro-orm.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { DynamicModule, Module } from '@nestjs/common';

import { createMikroOrmRepositoryProviders } from './mikro-orm.providers';
import { MikroOrmCoreModule } from './mikro-orm-core.module';
import { MikroOrmModuleAsyncOptions, MikroOrmModuleOptions } from './typings';
import { MikroOrmModuleAsyncOptions, MikroOrmModuleSyncOptions } from './typings';
import { REGISTERED_ENTITIES } from './mikro-orm.common';

@Module({})
export class MikroOrmModule {

static forRoot(options?: MikroOrmModuleOptions): DynamicModule {
static forRoot(options?: MikroOrmModuleSyncOptions): DynamicModule {
return {
module: MikroOrmModule,
imports: [MikroOrmCoreModule.forRoot(options)],
Expand Down
7 changes: 4 additions & 3 deletions src/mikro-orm.providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getRepositoryToken, logger, MIKRO_ORM_MODULE_OPTIONS, REGISTERED_ENTITI
import { AnyEntity, ConfigurationLoader, EntityManager, EntityName, MikroORM } from '@mikro-orm/core';

import { MikroOrmModuleAsyncOptions, MikroOrmModuleOptions, MikroOrmOptionsFactory } from './typings';
import { Provider } from '@nestjs/common';
import { Provider, Scope } from '@nestjs/common';

export const createMikroOrmProvider = (): Provider => ({
provide: MikroORM,
Expand All @@ -26,9 +26,10 @@ export const createMikroOrmProvider = (): Provider => ({
inject: [MIKRO_ORM_MODULE_OPTIONS],
});

export const createMikroOrmEntityManagerProvider = (alias?: string): Provider => ({
export const createMikroOrmEntityManagerProvider = (scope = Scope.DEFAULT, alias?: string): Provider => ({
provide: alias ?? EntityManager,
useFactory: (orm: MikroORM) => orm.em,
scope,
useFactory: (orm: MikroORM) => scope === Scope.DEFAULT ? orm.em : orm.em.fork(),
inject: [MikroORM],
});

Expand Down
10 changes: 8 additions & 2 deletions src/typings.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { IDatabaseDriver, Options } from '@mikro-orm/core';
import { MiddlewareConsumer, ModuleMetadata, Type } from '@nestjs/common';
import { MiddlewareConsumer, ModuleMetadata, Scope, Type } from '@nestjs/common';
import { AbstractHttpAdapter } from '@nestjs/core';

export interface NestMiddlewareConsumer extends MiddlewareConsumer {
httpAdapter: AbstractHttpAdapter;
}

type MikroOrmNestScopeOptions = {
scope?: Scope;
};

export type MikroOrmModuleOptions<D extends IDatabaseDriver = IDatabaseDriver> = {
registerRequestContext?: boolean;
autoLoadEntities?: boolean;
Expand All @@ -23,7 +27,9 @@ export interface MikroOrmOptionsFactory<D extends IDatabaseDriver = IDatabaseDri
createMikroOrmOptions(): Promise<MikroOrmModuleOptions<D>> | MikroOrmModuleOptions<D>;
}

export interface MikroOrmModuleAsyncOptions<D extends IDatabaseDriver = IDatabaseDriver> extends Pick<ModuleMetadata, 'imports' | 'providers'> {
export interface MikroOrmModuleSyncOptions extends MikroOrmModuleOptions, MikroOrmNestScopeOptions { }

export interface MikroOrmModuleAsyncOptions<D extends IDatabaseDriver = IDatabaseDriver> extends Pick<ModuleMetadata, 'imports' | 'providers'>, MikroOrmNestScopeOptions {
useExisting?: Type<MikroOrmOptionsFactory<D>>;
useClass?: Type<MikroOrmOptionsFactory<D>>;
useFactory?: (...args: any[]) => Promise<MikroOrmModuleOptions<D>> | MikroOrmModuleOptions<D>;
Expand Down
94 changes: 92 additions & 2 deletions tests/mikro-orm.module.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EntityManager, MikroORM, Options } from '@mikro-orm/core';
import { Inject, Logger, Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Inject, Logger, Module, Scope } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { MikroOrmModule, MikroOrmOptionsFactory } from '../src';

const testOptions: Options = {
Expand Down Expand Up @@ -28,6 +29,28 @@ class ConfigService implements MikroOrmOptionsFactory {
@Module({ providers: [ConfigService, myLoggerProvider], exports: [ConfigService] })
class ConfigModule { }

const getEntityManagerLoop = async (module: TestingModule): Promise<Set<string>> => {
// this function mocks the contextId factory which is called on each request
// it's looped 5 times and resolves the EntityManager provider 10 times
// set only allows unique values, it should only return 5 items as it should resolve the same em with the same contextId

const generatedIds = new Set<string>();

for(let i = 0; i < 5; i++) {
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);

(await Promise.all([
module.resolve(EntityManager, contextId),
module.resolve(EntityManager, contextId),
])).forEach(em => generatedIds.add(em.id));
}

return generatedIds;
}

describe('MikroORM Module', () => {

it('forRoot', async () => {
Expand Down Expand Up @@ -87,4 +110,71 @@ describe('MikroORM Module', () => {
await orm.close();
});

it('forRoot should return a new em each request with request scope', async () => {
const module = await Test.createTestingModule({
imports: [MikroOrmModule.forRoot({
...testOptions,
scope: Scope.REQUEST
})],
}).compile();

const idSet = await getEntityManagerLoop(module);

expect(idSet.size).toBe(5);

await module.get<MikroORM>(MikroORM).close();
});

it('forRootAsync should return a new em each request with request scope', async () => {
const module = await Test.createTestingModule({
imports: [MikroOrmModule.forRootAsync({
useFactory: (logger: Logger) => ({
...testOptions,
logger: logger.log.bind(logger),
}),
inject: ['my-logger'],
providers: [myLoggerProvider],
scope: Scope.REQUEST
})],
}).compile();

const idSet = await getEntityManagerLoop(module);

expect(idSet.size).toBe(5);

await module.get<MikroORM>(MikroORM).close();
});

it('forRoot should return the same em each request with default scope', async () => {
const module = await Test.createTestingModule({
imports: [MikroOrmModule.forRoot({
...testOptions,
})],
}).compile();

const idSet = await getEntityManagerLoop(module);

expect(idSet.size).toBe(1);

await module.get<MikroORM>(MikroORM).close();
});

it('forRootAsync should return the same em each request with default scope', async () => {
const module = await Test.createTestingModule({
imports: [MikroOrmModule.forRootAsync({
useFactory: (logger: Logger) => ({
...testOptions,
logger: logger.log.bind(logger),
}),
inject: ['my-logger'],
providers: [myLoggerProvider]
})],
}).compile();

const idSet = await getEntityManagerLoop(module);

expect(idSet.size).toBe(1);

await module.get<MikroORM>(MikroORM).close();
});
});

0 comments on commit c11e0ea

Please sign in to comment.