Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mikro-orm): manage lifecycle of subscribers #2293

Merged
28 changes: 28 additions & 0 deletions docs/tutorials/mikroorm.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,34 @@ export class UsersCtrl {
}
```

## Managing Lifecycle of Subscribers

With Ts.ED, managing the lifecycle of subscribers registered with Mikro-ORM using the IoC container is simple. To automatically resolve a subscriber's dependencies, you can use the `@Subscriber` decorator as follows:

```typescript
import {EventSubscriber} from "@mikro-orm/core";
import {Subscriber} from "@tsed/mikro-orm";

@Subscriber()
export class SomeSubscriber implements EventSubscriber {
// ...
}
```

In this example, we register the `SomeSubscriber` subscriber, which is automatically instantiated by the module using the IoC container, allowing you to easily manage the dependencies of your subscribers.

You can also specify the context name for a subscriber to tie it to a particular instance of the ORM:

```typescript
import {EventSubscriber} from "@mikro-orm/core";
import {Subscriber} from "@tsed/mikro-orm";

@Subscriber({contextName: "mongodb"})
export class SomeSubscriber implements EventSubscriber {
// ...
}
```

## Author

<GithubContributors :users="['derevnjuk']"/>
Expand Down
8 changes: 4 additions & 4 deletions packages/orm/mikro-orm/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module.exports = {
roots: ["<rootDir>/src", "<rootDir>/test"],
coverageThreshold: {
global: {
branches: 88.31,
functions: 94.73,
lines: 97.43,
statements: 97.88
branches: 87.17,
functions: 97.56,
lines: 98.24,
statements: 98.24
}
}
};
28 changes: 28 additions & 0 deletions packages/orm/mikro-orm/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,34 @@ export class UsersCtrl {
}
```

## Managing Lifecycle of Subscribers

With Ts.ED, managing the lifecycle of subscribers registered with Mikro-ORM using the IoC container is simple. To automatically resolve a subscriber's dependencies, you can use the `@Subscriber` decorator as follows:

```typescript
import {EventSubscriber} from "@mikro-orm/core";
import {Subscriber} from "@tsed/mikro-orm";

@Subscriber()
export class SomeSubscriber implements EventSubscriber {
// ...
}
```

In this example, we register the `SomeSubscriber` subscriber, which is automatically instantiated by the module using the IoC container, allowing you to easily manage the dependencies of your subscribers.

You can also specify the context name for a subscriber to tie it to a particular instance of the ORM:

```typescript
import {EventSubscriber} from "@mikro-orm/core";
import {Subscriber} from "@tsed/mikro-orm";
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved

@Subscriber({contextName: "mongodb"})
export class SomeSubscriber implements EventSubscriber {
// ...
}
```

## Contributors

Please read [contributing guidelines here](https://tsed.io/contributing.html)
Expand Down
35 changes: 17 additions & 18 deletions packages/orm/mikro-orm/src/MikroOrmModule.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import {EntityManager, MikroORM, Options} from "@mikro-orm/core";
import {EntityManager, EventSubscriber, MikroORM, Options} from "@mikro-orm/core";
import {PlatformTest} from "@tsed/common";
import {DIContext} from "@tsed/di";
import {anything, deepEqual, instance, mock, reset, verify, when} from "ts-mockito";
import {anyOfClass, anything, deepEqual, instance, mock, reset, verify, when} from "ts-mockito";
import {MikroOrmModule} from "./MikroOrmModule";
import {MikroOrmContext} from "./services/MikroOrmContext";
import {MikroOrmRegistry} from "./services/MikroOrmRegistry";
import {Subscriber} from "./decorators/subscriber";

class Subscriber1 implements EventSubscriber {}

@Subscriber()
class Subscriber2 implements EventSubscriber {}

describe("MikroOrmModule", () => {
const config: Options = {
type: "mongo",
entities: [],
clientUrl: "mongo://localhost"
clientUrl: "mongo://localhost",
subscribers: [new Subscriber1()]
};
const mockedMikroOrmRegistry = mock<MikroOrmRegistry>();
const mockedMikroOrmContext = mock<MikroOrmContext>();
const mockedMikroORM = mock<MikroORM>();
const mockedEntityManager = mock<EntityManager>();
const mockedDIContext = mock<DIContext>();

let mikroOrmModule!: MikroOrmModule;

Expand All @@ -41,34 +46,30 @@ describe("MikroOrmModule", () => {
afterEach(() => {
jest.resetAllMocks();

reset<MikroOrmRegistry | EntityManager | MikroORM | MikroOrmContext | DIContext>(
reset<MikroOrmRegistry | EntityManager | MikroORM | MikroOrmContext>(
mockedMikroOrmRegistry,
mockedMikroOrmContext,
mockedMikroORM,
mockedEntityManager,
mockedDIContext
mockedEntityManager
);

return PlatformTest.reset();
});

describe("$onInit", () => {
it("should register the corresponding instances", async () => {
// arrange
when(mockedMikroOrmRegistry.register(config)).thenResolve({} as unknown as MikroORM);

it("should register the subscribers", async () => {
// act
await mikroOrmModule.$onInit();

verify(mockedMikroOrmRegistry.register(deepEqual(config))).called();
// assert
verify(
mockedMikroOrmRegistry.register(deepEqual({...config, subscribers: [anyOfClass(Subscriber1), anyOfClass(Subscriber2)]}))
).called();
});
});

describe("$onDestroy", () => {
it("should destroy the corresponding instances", async () => {
// arrange
when(mockedMikroOrmRegistry.clear()).thenResolve();

// act
await mikroOrmModule.$onDestroy();

Expand All @@ -81,7 +82,6 @@ describe("MikroOrmModule", () => {
it("should return a function", () => {
// arrange
const next = jest.fn();
const diContext = instance(mockedDIContext);

// act
const result = mikroOrmModule.$alterRunInContext(next);
Expand All @@ -94,7 +94,6 @@ describe("MikroOrmModule", () => {
// arrange
const next = jest.fn();
const manager = instance(mockedEntityManager);
const diContext = instance(mockedDIContext);

when(mockedMikroOrmRegistry.values()).thenReturn([instance(mockedMikroORM)] as unknown as IterableIterator<MikroORM>);
when(mockedMikroORM.em).thenReturn(manager);
Expand Down
52 changes: 47 additions & 5 deletions packages/orm/mikro-orm/src/MikroOrmModule.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import "./services/MikroOrmFactory";
import {AlterRunInContext, Constant, DIContext, Inject, Module, OnDestroy, OnInit, registerProvider} from "@tsed/di";
import {Options} from "@mikro-orm/core";
import {
AlterRunInContext,
Constant,
Inject,
InjectorService,
LocalsContainer,
Module,
OnDestroy,
OnInit,
ProviderScope,
registerProvider
} from "@tsed/di";
import {EventSubscriber, Options} from "@mikro-orm/core";
import {MikroOrmRegistry} from "./services/MikroOrmRegistry";
import {RetryStrategy} from "./services/RetryStrategy";
import {OptimisticLockErrorFilter} from "./filters/OptimisticLockErrorFilter";
import {MikroOrmContext} from "./services/MikroOrmContext";
import {classOf, isFunction, Store} from "@tsed/core";
import {DEFAULT_CONTEXT_NAME, SUBSCRIBER_INJECTION_TYPE} from "./constants";

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace TsED {
interface Configuration {
/**
* The the ORM configuration, entity metadata.
* The ORM configuration, entity metadata.
* If you omit the `options` parameter, your CLI config will be used.
*/
mikroOrm?: Options[];
Expand All @@ -32,10 +45,15 @@ export class MikroOrmModule implements OnDestroy, OnInit, AlterRunInContext {
@Inject()
private readonly context!: MikroOrmContext;

@Inject()
private readonly injector!: InjectorService;

constructor(@Inject(SUBSCRIBER_INJECTION_TYPE) private subscribers: EventSubscriber[]) {}

public async $onInit(): Promise<void> {
const promises = this.settings.map((opts) => this.registry.register(opts));
const container = new LocalsContainer();

await Promise.all(promises);
await Promise.all(this.settings.map((opts) => this.registry.register({...opts, subscribers: this.getSubscribers(opts, container)})));
}

public $onDestroy(): Promise<void> {
Expand All @@ -46,6 +64,30 @@ export class MikroOrmModule implements OnDestroy, OnInit, AlterRunInContext {
return () => this.createContext(next);
}

private getSubscribers(opts: Pick<Options, "subscribers" | "contextName">, container: LocalsContainer) {
return [...this.getUnmanagedSubscribers(opts, container), ...this.getManagedSubscribers(opts.contextName)];
}

private getUnmanagedSubscribers(opts: Pick<Options, "subscribers">, container: LocalsContainer) {
const diOpts = {scope: ProviderScope.INSTANCE};

return (opts.subscribers ?? []).map((subscriber) => {
// Starting from https://github.com/mikro-orm/mikro-orm/issues/4231 mikro-orm
// accept also accepts class reference, not just instances.
if (isFunction(subscriber)) {
return this.injector.invoke(subscriber, container, diOpts);
}

this.injector.bindInjectableProperties(subscriber, container, diOpts);

return subscriber;
});
}

private getManagedSubscribers(contextName: string = DEFAULT_CONTEXT_NAME): EventSubscriber[] {
return this.subscribers.filter((instance) => Store.from(classOf(instance)).get(SUBSCRIBER_INJECTION_TYPE)?.contextName === contextName);
}

private createContext(next: (...args: unknown[]) => unknown): unknown {
const instances = [...this.registry.values()];
const managers = instances.map((orm) => orm.em);
Expand Down
2 changes: 2 additions & 0 deletions packages/orm/mikro-orm/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_CONTEXT_NAME = "default";
export const SUBSCRIBER_INJECTION_TYPE = Symbol("Subscriber");
derevnjuk marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 26 additions & 0 deletions packages/orm/mikro-orm/src/decorators/subscriber.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Store} from "@tsed/core";
import {EventSubscriber} from "@mikro-orm/core";
import {Subscriber} from "./subscriber";
import {DEFAULT_CONTEXT_NAME, SUBSCRIBER_INJECTION_TYPE} from "../constants";

@Subscriber()
export class Subscriber1 implements EventSubscriber {}

@Subscriber({contextName: "non-default"})
export class Subscriber2 implements EventSubscriber {}

describe("@Subscriber", () => {
it("should decorate the class", () => {
// act
const result = Store.from(Subscriber1).get(SUBSCRIBER_INJECTION_TYPE);
// assert
expect(result).toEqual({contextName: DEFAULT_CONTEXT_NAME});
});

it("should decorate the class tying a context name to it", () => {
// act
const result = Store.from(Subscriber2).get(SUBSCRIBER_INJECTION_TYPE);
// assert
expect(result).toEqual({contextName: "non-default"});
});
});
11 changes: 11 additions & 0 deletions packages/orm/mikro-orm/src/decorators/subscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {StoreSet, useDecorators} from "@tsed/core";
import {Injectable} from "@tsed/di";
import {DEFAULT_CONTEXT_NAME, SUBSCRIBER_INJECTION_TYPE} from "../constants";

/**
* Register a new subscriber for the given context name.
* @decorator
* @mikroOrm
*/
export const Subscriber = (options: {contextName: string} = {contextName: DEFAULT_CONTEXT_NAME}) =>
useDecorators(Injectable({type: SUBSCRIBER_INJECTION_TYPE}), StoreSet(SUBSCRIBER_INJECTION_TYPE, options));
2 changes: 2 additions & 0 deletions packages/orm/mikro-orm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
*/

export * from "./MikroOrmModule";
export * from "./constants";
export * from "./decorators/entityManager";
export * from "./decorators/orm";
export * from "./decorators/subscriber";
export * from "./decorators/transactional";
export * from "./filters/OptimisticLockErrorFilter";
export * from "./interceptors/TransactionalInterceptor";
Expand Down
2 changes: 1 addition & 1 deletion packages/orm/mikro-orm/test/helpers/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {Configuration, Inject} from "@tsed/di";
import "@tsed/platform-express";
import bodyParser from "body-parser";
import compress from "compression";

import cookieParser from "cookie-parser";
import filedirname from "filedirname";
import methodOverride from "method-override";
import "./services/ManagedEventSubscriber";

// FIXME remove when esm is ready
const [, rootDir] = filedirname();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {EventSubscriber, TransactionEventArgs} from "@mikro-orm/core";
import {Inject} from "@tsed/di";
import {Logger} from "@tsed/logger";
import {Subscriber} from "../../../src";

@Subscriber()
export class ManagedEventSubscriber implements EventSubscriber {
constructor(@Inject() private readonly logger: Logger) {}

public afterFlush(_: TransactionEventArgs): Promise<void> {
this.logger.info("Changes has been flushed.");
return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {EventSubscriber, TransactionEventArgs} from "@mikro-orm/core";
import {$log, Logger} from "@tsed/logger";
import {Inject} from "@tsed/di";

export class UnmanagedEventSubscriber1 implements EventSubscriber {
constructor(@Inject() private readonly logger: Logger) {}

public afterFlush(_: TransactionEventArgs): Promise<void> {
$log.info("Changes has been flushed.");
return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {EventSubscriber, TransactionEventArgs} from "@mikro-orm/core";
import {$log, Logger} from "@tsed/logger";
import {Inject} from "@tsed/di";

export class UnmanagedEventSubscriber2 implements EventSubscriber {
@Inject()
private readonly logger: Logger;

public afterFlush(_: TransactionEventArgs): Promise<void> {
$log.info("Changes has been flushed.");
return Promise.resolve();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {EntityManager, MikroORM} from "@mikro-orm/core";
import {Injectable} from "@tsed/di";
import {Em, Orm, Transactional} from "../../../src/index";
import {Em, Orm, Transactional} from "../../../src";
import {User} from "../entity/User";

@Injectable()
Expand Down