Skip to content

Commit

Permalink
feat(core): add @EnsureRequestContext decorator + rename `@UseReque…
Browse files Browse the repository at this point in the history
…stContext`

Similar to `@UseRequestContext` but reuses existing context if available.

Closes #4009
  • Loading branch information
B4nan committed Nov 5, 2023
1 parent a3b43e9 commit 5e088ae
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 28 deletions.
16 changes: 11 additions & 5 deletions docs/docs/identity-map.md
Expand Up @@ -82,23 +82,25 @@ The `RequestContext.getEntityManager()` method then checks `AsyncLocalStorage` s

The [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) class from Node.js core is the magician here. It allows us to track the context throughout the async calls. It allows us to decouple the `EntityManager` fork creation (usually in a middleware as shown in previous section) from its usage through the global `EntityManager` instance.

## `@UseRequestContext()` decorator
## `@CreateRequestContext()` decorator

> 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).

We can use the `@UseRequestContext()` 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.
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.createAsync()` call. Every call to such method will create new context (new `EntityManager` fork) which will be used inside.

> `@UseRequestContext()` 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.
> `@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 {

constructor(private readonly orm: MikroORM) { }

@UseRequestContext()
@CreateRequestContext()
async doSomething() {
// this will be executed in a separate context
}
Expand All @@ -113,14 +115,18 @@ import { DI } from '..';

export class MyService {

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

}
```

## `@EnsureRequestContext()` decorator

Sometimes you may prefer to just ensure the method is executed inside a request context, and reuse the existing context if available. You can use the `@EnsureRequestContext()` decorator here, it behaves exactly like the `@CreateRequestContext`, but only creates new context if necessary, reusing the existing one if possible.

## Why is Request Context needed?

Imagine we will use a single Identity Map throughout our application. It will be shared across all request handlers, that can possibly run in parallel.
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/upgrading-v5-to-v6.md
Expand Up @@ -196,3 +196,8 @@ Use `RequestContext.create` instead, it can be awaited now.
-const ret = await RequestContext.createAsync(em, async () => { ... });
+const ret = await RequestContext.create(em, async () => { ... });
```

## Renamed `@UseRequestContext()`

The decorator was renamed to `@CreateRequestContext()` to make it clear it always creates new context, and a new `@EnsureRequestContext()` decorator was added that will reuse existing contexts if available.

22 changes: 13 additions & 9 deletions docs/docs/usage-with-nestjs.md
Expand Up @@ -191,40 +191,40 @@ With that option specified, every entity registered through the `forFeature()` m
## Request scoped handlers in queues

> `@UseRequestContext()` decorator was added in v4.1.0
> `@CreateRequestContext()` decorator is available in `@mikro-orm/core` package.
> Since v5, `@UseRequestContext()` decorator is available in the `@mikro-orm/core` package. It is valid approach not just for nestjs projects.
> Before v6, `@CreateRequestContext()` was called `@UseRequestContext()`.
As mentioned in the [docs](identity-map.md), we need a clean state for each request. That is handled automatically thanks to the `RequestContext` helper registered via middleware.

But middlewares are executed only for regular HTTP request handles, what if we need a request scoped method outside of that? One example of that is queue handlers or scheduled tasks.

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

> `@UseRequestContext()` 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.
> `@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
@Controller()
export class MyService {

constructor(private readonly orm: MikroORM) { }

@UseRequestContext()
@CreateRequestContext()
async doSomething() {
// this will be executed in a separate context
}

}
```

Alternatively we can provide a callback that will return the `MikroORM` instance.
Alternatively you can provide a callback that will return the `MikroORM` instance.

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

export class MyService {

@UseRequestContext(() => DI.orm)
@CreateRequestContext(() => DI.orm)
async doSomething() {
// this will be executed in a separate context
}
Expand All @@ -246,14 +246,18 @@ export class MyConsumer {
await this.doSomethingWithMikro();
}

@UseRequestContext()
@CreateRequestContext()
async doSomethingWithMikro() {
// this will be executed in a separate context
}
}
```

As in this case, the `@Process()` decorator expects to receive an executable function, but if we add `@UseRequestContext()` to the handler as well, if `@UseRequestContext()` is executed before `@Process()`, the later will receive `void`.
As in this case, the `@Process()` decorator expects to receive an executable function, but if we add `@CreateRequestContext()` to the handler as well, if `@CreateRequestContext()` is executed before `@Process()`, the later will receive `void`.

## `@EnsureRequestContext()` decorator

Sometimes you may prefer to just ensure the method is executed inside a request context, and reuse the existing context if available. You can use the `@EnsureRequestContext()` decorator here, it behaves exactly like the `@CreateRequestContext`, but only creates new context if necessary, reusing the existing one if possible.

## Request scoping when using GraphQL

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/decorators/CreateRequestContext.ts
Expand Up @@ -20,7 +20,7 @@ export function CreateRequestContext<T>(getContext?: MikroORM | Promise<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)`');
}

return await RequestContext.createAsync(orm.em, async () => {
return await RequestContext.create(orm.em, () => {
return originalMethod.apply(this, args);
});
};
Expand Down
@@ -1,16 +1,20 @@
import { MikroORM } from '../MikroORM';
import { RequestContext } from '../utils/RequestContext';

/** @deprecated use `@CreateRequestContext()` instead, `@UseRequestContext()` will be removed in v6 */
export function UseRequestContext<T>(getContext?: MikroORM | ((type?: T) => MikroORM)): MethodDecorator {
export function EnsureRequestContext<T>(getContext?: MikroORM | ((type?: T) => MikroORM)): 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 */
const orm = getContext instanceof MikroORM ? getContext : (getContext?.(this) ?? (this as any).orm);

if (!(orm as unknown instanceof MikroORM)) {
throw new Error('@UseRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@UseRequestContext(() => orm)`');
throw new Error('@EnsureRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@EnsureRequestContext(() => orm)`');
}

return await RequestContext.create(orm.em, () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/decorators/index.ts
Expand Up @@ -14,6 +14,6 @@ export * from './Embeddable';
export * from './Embedded';
export * from './Filter';
export * from './Subscriber';
export * from './UseRequestContext';
export * from './CreateRequestContext';
export * from './EnsureRequestContext';
export * from './hooks';
22 changes: 13 additions & 9 deletions tests/decorators.test.ts
Expand Up @@ -9,8 +9,9 @@ import {
ReferenceKind,
Utils,
Subscriber,
UseRequestContext,
CreateRequestContext,
EnsureRequestContext,
RequestContext,
} from '@mikro-orm/core';
import type { Dictionary } from '@mikro-orm/core';
import { Test } from './entities';
Expand Down Expand Up @@ -71,27 +72,27 @@ class TestClass2 {

constructor(private readonly orm: MikroORM) {}

@UseRequestContext()
@EnsureRequestContext()
async asyncMethodReturnsValue() {
return TEST_VALUE;
}

@UseRequestContext()
@EnsureRequestContext()
methodReturnsValue() {
return TEST_VALUE;
}

@UseRequestContext()
@EnsureRequestContext()
async asyncMethodReturnsNothing() {
//
}

@UseRequestContext()
@EnsureRequestContext()
methodReturnsNothing() {
//
}

@UseRequestContext(() => DI.orm)
@EnsureRequestContext(() => DI.orm)
methodWithCallback() {
//
}
Expand All @@ -109,7 +110,6 @@ class TestClass3 {

}


describe('decorators', () => {

const lookupPathFromDecorator = jest.spyOn(Utils, 'lookupPathFromDecorator');
Expand Down Expand Up @@ -209,7 +209,7 @@ describe('decorators', () => {
expect(ret9).toBeUndefined();
});

test('UseRequestContext', async () => {
test('EnsureRequestContext', async () => {
const orm = Object.create(MikroORM.prototype, { em: { value: { name: 'default', fork: jest.fn() } } });
const test = new TestClass2(orm);

Expand All @@ -230,8 +230,12 @@ describe('decorators', () => {
const ret6 = await test2.methodWithCallback();
expect(ret6).toBeUndefined();

const err = '@UseRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@UseRequestContext(() => orm)`';
const err = '@EnsureRequestContext() decorator can only be applied to methods of classes with `orm: MikroORM` property, or with a callback parameter like `@EnsureRequestContext(() => orm)`';
await expect(test2.asyncMethodReturnsValue()).rejects.toThrow(err);

await RequestContext.create(orm.em, async () => {
await expect(test2.asyncMethodReturnsValue()).resolves.toBe(TEST_VALUE);
});
});

});

0 comments on commit 5e088ae

Please sign in to comment.