Skip to content

Commit

Permalink
feat(core): allow passing EntityManager or EntityRepository to `@…
Browse files Browse the repository at this point in the history
…CreateRequestContext` decorator
  • Loading branch information
B4nan committed Mar 24, 2024
1 parent 21377cf commit 184cdd4
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 47 deletions.
27 changes: 20 additions & 7 deletions docs/docs/identity-map.md
Expand Up @@ -109,18 +109,16 @@ app.use((req, res, next) => {

> Before v6, `@CreateRequestContext()` was called `@UseRequestContext()`.
Middlewares are executed only for regular HTTP request handlers, what if we need a request scoped method outside that? One example of that is queue handlers or scheduled tasks (e.g. CRON jobs).
Middlewares are executed only for regular HTTP request handlers, what if you need a request scoped method outside that? One example of that is queue handlers or scheduled tasks (e.g. CRON jobs).

We can use the `@CreateRequestContext()` decorator. It requires us to first inject the `MikroORM` instance to current context, it will be then used to create the context for us. Under the hood, the decorator will register new request context for our method and execute it inside the context.

This decorator will wrap the underlying method in `RequestContext.create()` call. Every call to such method will create new context (new `EntityManager` fork) which will be used inside.
In those cases, you can use the `@CreateRequestContext()` decorator. It requires you to first inject the `MikroORM` instance (or an `EntityManager` or some `EntityRepository`) to current context, it will be then used to create a new context for us. Under the hood, the decorator will register the new request context for our method and execute it inside it (vi `RequestContext.create()`).

> `@CreateRequestContext()` should be used only on the top level methods. It should not be nested - a method decorated with it should not call another method that is also decorated with it.
```ts
@Injectable()
export class MyService {

// or `private readonly em: EntityManager`
constructor(private readonly orm: MikroORM) { }

@CreateRequestContext()
Expand All @@ -131,14 +129,29 @@ export class MyService {
}
```

Alternatively we can provide a callback that will return the `MikroORM` instance.
Alternatively you can provide a callback that will return one of `MikroORM`, `EntityManager` or `EntityRepository` instance.

```ts
import { DI } from '..';

export class MyService {

@CreateRequestContext(() => DI.orm)
@CreateRequestContext(() => DI.em)
async doSomething() {
// this will be executed in a separate context
}

}
```

The callback will receive current `this` in the first parameter. You can use it to link the `EntityManager` or `EntityRepository` too:

```ts
export class MyService {

constructor(private readonly userRepository: EntityRepository<User>) { }

@CreateRequestContext<MyService>(t => t.userRepository)
async doSomething() {
// this will be executed in a separate context
}
Expand Down
27 changes: 20 additions & 7 deletions docs/versioned_docs/version-6.1/identity-map.md
Expand Up @@ -109,18 +109,16 @@ app.use((req, res, next) => {

> Before v6, `@CreateRequestContext()` was called `@UseRequestContext()`.
Middlewares are executed only for regular HTTP request handlers, what if we need a request scoped method outside that? One example of that is queue handlers or scheduled tasks (e.g. CRON jobs).
Middlewares are executed only for regular HTTP request handlers, what if you need a request scoped method outside that? One example of that is queue handlers or scheduled tasks (e.g. CRON jobs).

We can use the `@CreateRequestContext()` decorator. It requires us to first inject the `MikroORM` instance to current context, it will be then used to create the context for us. Under the hood, the decorator will register new request context for our method and execute it inside the context.

This decorator will wrap the underlying method in `RequestContext.create()` call. Every call to such method will create new context (new `EntityManager` fork) which will be used inside.
In those cases, you can use the `@CreateRequestContext()` decorator. It requires you to first inject the `MikroORM` instance (or an `EntityManager` or some `EntityRepository`) to current context, it will be then used to create a new context for us. Under the hood, the decorator will register the new request context for our method and execute it inside it (vi `RequestContext.create()`).

> `@CreateRequestContext()` should be used only on the top level methods. It should not be nested - a method decorated with it should not call another method that is also decorated with it.
```ts
@Injectable()
export class MyService {

// or `private readonly em: EntityManager`
constructor(private readonly orm: MikroORM) { }

@CreateRequestContext()
Expand All @@ -131,14 +129,29 @@ export class MyService {
}
```

Alternatively we can provide a callback that will return the `MikroORM` instance.
Alternatively you can provide a callback that will return one of `MikroORM`, `EntityManager` or `EntityRepository` instance.

```ts
import { DI } from '..';

export class MyService {

@CreateRequestContext(() => DI.orm)
@CreateRequestContext(() => DI.em)
async doSomething() {
// this will be executed in a separate context
}

}
```

The callback will receive current `this` in the first parameter. You can use it to link the `EntityManager` or `EntityRepository` too:

```ts
export class MyService {

constructor(private readonly userRepository: EntityRepository<User>) { }

@CreateRequestContext<MyService>(t => t.userRepository)
async doSomething() {
// this will be executed in a separate context
}
Expand Down
30 changes: 28 additions & 2 deletions packages/core/src/decorators/CreateRequestContext.ts
@@ -1,23 +1,45 @@
import { MikroORM } from '../MikroORM';
import { RequestContext } from '../utils/RequestContext';
import { EntityManager } from '../EntityManager';
import { EntityRepository } from '../entity/EntityRepository';

export function CreateRequestContext<T>(getContext?: MikroORM | Promise<MikroORM> | ((type?: T) => MikroORM | Promise<MikroORM>)): MethodDecorator {
export function CreateRequestContext<T>(getContext?: MikroORM | Promise<MikroORM> | ((type: T) => MikroORM | Promise<MikroORM> | EntityManager | EntityRepository<any>), respectExistingContext = false): MethodDecorator {
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (this: T, ...args: any[]) {
// reuse existing context if available
if (RequestContext.currentRequestContext()) {
return originalMethod.apply(this, args);
}

/* istanbul ignore next */
let orm: unknown;
let em: unknown;

if (typeof getContext === 'function') {
orm = await (getContext(this) ?? (this as any).orm);
} else if (getContext) {
orm = await getContext;
} else {
orm = await (this as any).orm;
em = await (this as any).em;
}

if (em instanceof EntityManager) {
return await RequestContext.create(em, () => {
return originalMethod.apply(this, args);
});
}

if (orm instanceof EntityRepository) {
return await RequestContext.create(orm.getEntityManager(), () => {
return originalMethod.apply(this, args);
});
}

if (!(orm instanceof MikroORM)) {
throw new Error('@CreateRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@CreateRequestContext(() => orm)`');
const name = respectExistingContext ? 'EnsureRequestContext' : 'CreateRequestContext';
throw new Error(`@${name}() decorator can only be applied to methods of classes with \`orm: MikroORM\` property, \`em: EntityManager\` property, or with a callback parameter like \`@${name}(() => orm)\` that returns one of those types. The parameter will contain a reference to current \`this\`. Returning an EntityRepository from it is also supported.`);
}

return await RequestContext.create(orm.em, () => {
Expand All @@ -28,3 +50,7 @@ export function CreateRequestContext<T>(getContext?: MikroORM | Promise<MikroORM
return descriptor;
};
}

export function EnsureRequestContext<T>(getContext?: MikroORM | Promise<MikroORM> | ((type: T) => MikroORM | Promise<MikroORM> | EntityManager | EntityRepository<any>)): MethodDecorator {
return CreateRequestContext(getContext, true);
}
27 changes: 0 additions & 27 deletions packages/core/src/decorators/EnsureRequestContext.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/core/src/decorators/index.ts
Expand Up @@ -14,5 +14,4 @@ export * from './Embeddable';
export * from './Embedded';
export * from './Filter';
export * from './CreateRequestContext';
export * from './EnsureRequestContext';
export * from './hooks';
40 changes: 37 additions & 3 deletions tests/decorators.test.ts
Expand Up @@ -11,6 +11,8 @@ import {
CreateRequestContext,
EnsureRequestContext,
RequestContext,
EntityManager,
EntityRepository,
} from '@mikro-orm/core';
import type { Dictionary } from '@mikro-orm/core';
import { Test } from './entities';
Expand Down Expand Up @@ -109,6 +111,28 @@ class TestClass3 {

}

class TestClass4 {

constructor(private readonly em: EntityManager) {}

@CreateRequestContext()
foo() {
//
}

}

class TestClass5 {

constructor(private readonly repo: EntityRepository<any>) {}

@CreateRequestContext<TestClass5>(t => t.repo)
foo() {
//
}

}

describe('decorators', () => {

const lookupPathFromDecorator = jest.spyOn(Utils, 'lookupPathFromDecorator');
Expand Down Expand Up @@ -173,7 +197,9 @@ describe('decorators', () => {
});

test('CreateRequestContext', async () => {
const orm = Object.create(MikroORM.prototype, { em: { value: { name: 'default', fork: jest.fn() } } });
const em = Object.create(EntityManager.prototype, { name: { value: 'default' }, fork: { value: jest.fn() } });
const repo = Object.create(EntityRepository.prototype, { em: { value: em } });
const orm = Object.create(MikroORM.prototype, { em: { value: em } });
const test = new TestClass(orm);

const ret1 = await test.asyncMethodReturnsValue();
Expand All @@ -193,7 +219,7 @@ describe('decorators', () => {
const ret6 = await test2.methodWithCallback();
expect(ret6).toBeUndefined();

const err = '@CreateRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@CreateRequestContext(() => orm)`';
const err = '@CreateRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, `em: EntityManager` property, or with a callback parameter like `@CreateRequestContext(() => orm)` that returns one of those types. The parameter will contain a reference to current `this`. Returning an EntityRepository from it is also supported.';
await expect(test2.asyncMethodReturnsValue()).rejects.toThrow(err);
const ret7 = await test.methodWithAsyncCallback();
expect(ret7).toEqual(TEST_VALUE);
Expand All @@ -203,6 +229,14 @@ describe('decorators', () => {
const test3 = new TestClass3(ASYNC_ORM);
const ret9 = await test3.methodWithAsyncOrmPropertyAndReturnsNothing();
expect(ret9).toBeUndefined();

const test4 = new TestClass4(em);
const ret10 = await test4.foo();
expect(ret10).toBeUndefined();

const test5 = new TestClass5(repo);
const ret11 = await test5.foo();
expect(ret11).toBeUndefined();
});

test('EnsureRequestContext', async () => {
Expand All @@ -226,7 +260,7 @@ describe('decorators', () => {
const ret6 = await test2.methodWithCallback();
expect(ret6).toBeUndefined();

const err = '@EnsureRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@EnsureRequestContext(() => orm)`';
const err = '@EnsureRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, `em: EntityManager` property, or with a callback parameter like `@EnsureRequestContext(() => orm)` that returns one of those types. The parameter will contain a reference to current `this`. Returning an EntityRepository from it is also supported.';
await expect(test2.asyncMethodReturnsValue()).rejects.toThrow(err);

await RequestContext.create(orm.em, async () => {
Expand Down

0 comments on commit 184cdd4

Please sign in to comment.