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(core): add support for event subscribers #614

Merged
merged 1 commit into from
Jun 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/docs/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,18 @@ export class CustomAuthorRepository extends EntityRepository<Author> {
// your custom methods...
}
```

## Event Subscriber

### @Subscriber()

Used to register an event subscriber. Keep in mind that you need to make sure the file
gets loaded in order to make this decorator registration work (e.g. you import that file
explicitly somewhere).

```typescript
@Subscriber()
export class AuthorSubscriber implements EventSubscriber<Author> {
// ...
}
```
70 changes: 69 additions & 1 deletion docs/docs/lifecycle-hooks.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
---
title: Lifecycle Hooks
title: Lifecycle Hooks and EventSubscriber
sidebar_label: Hooks and Events
---

There are two ways to hook to the lifecycle of an entity:

- **Lifecycle hooks** are methods defined on the entity prototype.
- **EventSubscriber**s are classes that can be used to hook to multiple entities
or when you do not want to have the method present on the entity prototype.

## Hooks

You can use lifecycle hooks to run some code when entity gets persisted. You can mark any of
entity methods with them, you can also mark multiple methods with same hook.

Expand Down Expand Up @@ -34,3 +43,62 @@ locking errors.

> The **internal** instance of `EntityManager` accessible under `wrap(this).__em` is
> not meant for public usage.

## EventSubscriber

Use `EventSubscriber` to hook to multiple entities or if you do not want to pollute
the entity prototype. All methods are optional, if you omit the `getSubscribedEntities()`
method, it means you are subscribing to all entities.

You can either register the subscribers manually in the ORM configuration (via
`subscribers` array where you put the instance):

```typescript
MikroORM.init({
subscribers: [new AuthorSubscriber()],
});
```

Or use `@Subscriber()` decorator - keep in mind that you need to make sure the file gets
loaded in order to make this decorator registration work (e.g. you import that file
explicitly somewhere).

```typescript
import { EntityName, EventArgs, EventSubscriber, Subscriber } from '@mikro-orm/core';

@Subscriber()
export class AuthorSubscriber implements EventSubscriber<Author> {

getSubscribedEntities(): EntityName<Author2>[] {
return [Author2];
}

async afterCreate(args: EventArgs<Author2>): Promise<void> {
// ...
}

async afterUpdate(args: EventArgs<Author2>): Promise<void> {
// ...
}

}
```

Another example, where we register to all the events and all entities:

```typescript
import { EventArgs, EventSubscriber, Subscriber } from '@mikro-orm/core';

@Subscriber()
export class EverythingSubscriber implements EventSubscriber {

async afterCreate<T>(args: EventArgs<T>): Promise<void> { ... }
async afterDelete<T>(args: EventArgs<T>): Promise<void> { ... }
async afterUpdate<T>(args: EventArgs<T>): Promise<void> { ... }
async beforeCreate<T>(args: EventArgs<T>): Promise<void> { ... }
async beforeDelete<T>(args: EventArgs<T>): Promise<void> { ... }
async beforeUpdate<T>(args: EventArgs<T>): Promise<void> { ... }
onInit<T>(args: EventArgs<T>): void { ... }

}
```
11 changes: 11 additions & 0 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { v4 as uuid } from 'uuid';
import { inspect } from 'util';

import { Configuration, RequestContext, SmartQueryHelper, Utils, ValidationError } from './utils';
import { EntityAssigner, EntityFactory, EntityLoader, EntityRepository, EntityValidator, IdentifiedReference, LoadStrategy, Reference, ReferenceType, wrap } from './entity';
Expand All @@ -8,6 +9,7 @@ import { AnyEntity, Constructor, Dictionary, EntityData, EntityMetadata, EntityN
import { QueryOrderMap } from './enums';
import { MetadataStorage } from './metadata';
import { Transaction } from './connections';
import { EventManager } from './events';

/**
* The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
Expand All @@ -21,6 +23,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
private readonly entityLoader: EntityLoader = new EntityLoader(this);
private readonly unitOfWork = new UnitOfWork(this);
private readonly entityFactory = new EntityFactory(this.unitOfWork, this);
private readonly eventManager = new EventManager(this.config.get('subscribers'));
private transactionContext?: Transaction;

constructor(readonly config: Configuration,
Expand Down Expand Up @@ -546,6 +549,10 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
return em.entityFactory;
}

getEventManager(): EventManager {
return this.eventManager;
}

/**
* Checks whether this EntityManager is currently operating inside a database transaction.
*/
Expand Down Expand Up @@ -644,6 +651,10 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
});
}

[inspect.custom]() {
return `[EntityManager<${this.id}>]`;
}

}

export interface FindOneOrFailOptions<T> extends FindOneOptions<T> {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/decorators/Subscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Constructor } from '../typings';
import { MetadataStorage } from '../metadata';
import { EventSubscriber } from '../events';

export function Subscriber() {
return function (target: Constructor<EventSubscriber>) {
const subscribers = MetadataStorage.getSubscriberMetadata();
subscribers[target.name] = new target();
};
}
18 changes: 9 additions & 9 deletions packages/core/src/decorators/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MetadataStorage } from '../metadata';
import { HookType } from '../typings';
import { EventType } from '../events';

function hook(type: HookType) {
function hook(type: EventType) {
return function (target: any, method: string) {
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor);

Expand All @@ -14,35 +14,35 @@ function hook(type: HookType) {
}

export function BeforeCreate() {
return hook('beforeCreate');
return hook(EventType.beforeCreate);
}

export function AfterCreate() {
return hook('afterCreate');
return hook(EventType.afterCreate);
}

export function BeforeUpdate() {
return hook('beforeUpdate');
return hook(EventType.beforeUpdate);
}

export function AfterUpdate() {
return hook('afterUpdate');
return hook(EventType.afterUpdate);
}

export function OnInit() {
return hook('onInit');
return hook(EventType.onInit);
}

/**
* Called before deleting entity, but only when providing initialized entity to EM#remove()
*/
export function BeforeDelete() {
return hook('beforeDelete');
return hook(EventType.beforeDelete);
}

/**
* Called after deleting entity, but only when providing initialized entity to EM#remove()
*/
export function AfterDelete() {
return hook('afterDelete');
return hook(EventType.afterDelete);
}
1 change: 1 addition & 0 deletions packages/core/src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from './Indexed';
export * from './Repository';
export * from './Embeddable';
export * from './Embedded';
export * from './Subscriber';
export * from './hooks';
4 changes: 3 additions & 1 deletion packages/core/src/entity/EntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Utils } from '../utils';
import { EntityData, EntityMetadata, EntityName, EntityProperty, Primary } from '../typings';
import { UnitOfWork } from '../unit-of-work';
import { ReferenceType } from './enums';
import { EntityManager, wrap } from '..';
import { EntityManager, EventType, wrap } from '..';

export const SCALAR_TYPES = ['string', 'number', 'boolean', 'Date'];

Expand Down Expand Up @@ -146,6 +146,8 @@ export class EntityFactory {
if (meta.hooks && meta.hooks.onInit && meta.hooks.onInit.length > 0) {
meta.hooks.onInit.forEach(hook => (entity[hook] as unknown as () => void)());
}

this.em.getEventManager().dispatchEvent(EventType.onInit, entity, this.em);
}

}
54 changes: 54 additions & 0 deletions packages/core/src/events/EventManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AnyEntity } from '../typings';
import { EntityManager } from '../EntityManager';
import { EventSubscriber } from './EventSubscriber';
import { Utils } from '../utils';
import { EventType } from './EventType';

export class EventManager {

private readonly listeners: Partial<Record<EventType, EventSubscriber[]>> = {};
private readonly entities: Map<EventSubscriber, string[]> = new Map();

constructor(subscribers: EventSubscriber[]) {
subscribers.forEach(subscriber => this.registerSubscriber(subscriber));
}

registerSubscriber(subscriber: EventSubscriber): void {
this.entities.set(subscriber, this.getSubscribedEntities(subscriber));
Object.keys(EventType)
.filter(event => event in subscriber)
.forEach(event => {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(subscriber);
});
}

dispatchEvent(event: EventType.onInit, entity: AnyEntity, em: EntityManager): unknown;
dispatchEvent(event: EventType, entity: AnyEntity, em: EntityManager): Promise<unknown>;
dispatchEvent(event: EventType, entity: AnyEntity, em: EntityManager): Promise<unknown> | unknown {
const listeners: EventSubscriber[] = [];

for (const listener of this.listeners[event] || []) {
const entities = this.entities.get(listener)!;

if (entities.length === 0 || entities.includes(entity.constructor.name)) {
listeners.push(listener);
}
}

if (event === EventType.onInit) {
return listeners.forEach(listener => listener[event]!({ em, entity }));
}

return Utils.runSerial(listeners, listener => listener[event]!({ em, entity }));
}

private getSubscribedEntities(listener: EventSubscriber): string[] {
if (!listener.getSubscribedEntities) {
return [];
}

return listener.getSubscribedEntities().map(name => Utils.className(name));
}

}
18 changes: 18 additions & 0 deletions packages/core/src/events/EventSubscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AnyEntity, EntityName } from '../typings';
import { EntityManager } from '../EntityManager';

export interface EventArgs<T> {
entity: T;
em: EntityManager;
}

export interface EventSubscriber<T = AnyEntity> {
getSubscribedEntities?(): EntityName<T>[];
onInit?(args: EventArgs<T>): void;
beforeCreate?(args: EventArgs<T>): Promise<void>;
afterCreate?(args: EventArgs<T>): Promise<void>;
beforeUpdate?(args: EventArgs<T>): Promise<void>;
afterUpdate?(args: EventArgs<T>): Promise<void>;
beforeDelete?(args: EventArgs<T>): Promise<void>;
afterDelete?(args: EventArgs<T>): Promise<void>;
}
9 changes: 9 additions & 0 deletions packages/core/src/events/EventType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum EventType {
onInit = 'onInit',
beforeCreate = 'beforeCreate',
afterCreate = 'afterCreate',
beforeUpdate = 'beforeUpdate',
afterUpdate = 'afterUpdate',
beforeDelete = 'beforeDelete',
afterDelete = 'afterDelete',
}
3 changes: 3 additions & 0 deletions packages/core/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './EventType';
export * from './EventSubscriber';
export * from './EventManager';
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './enums';
export * from './exceptions';
export * from './MikroORM';
export * from './entity';
export * from './events';
export * from './EntityManager';
export * from './unit-of-work';
export * from './utils';
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { EntityMetadata, AnyEntity, Dictionary } from '../typings';
import { Utils, ValidationError } from '../utils';
import { EntityManager } from '../EntityManager';
import { EntityHelper } from '../entity';
import { EventSubscriber } from '../events';

export class MetadataStorage {

private static readonly metadata: Dictionary<EntityMetadata> = {};
private static readonly subscribers: Dictionary<EventSubscriber> = {};
private readonly metadata: Dictionary<EntityMetadata>;

constructor(metadata: Dictionary<EntityMetadata> = {}) {
Expand Down Expand Up @@ -36,6 +38,10 @@ export class MetadataStorage {
return meta;
}

static getSubscriberMetadata(): Dictionary<EventSubscriber> {
return MetadataStorage.subscribers;
}

static init(): MetadataStorage {
return new MetadataStorage(MetadataStorage.metadata);
}
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/typings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { QueryOrder } from './enums';
import { AssignOptions, Cascade, Collection, EntityRepository, EntityValidator, IdentifiedReference, Reference, ReferenceType, LoadStrategy } from './entity';
import { AssignOptions, Cascade, Collection, EntityRepository, EntityValidator, IdentifiedReference, LoadStrategy, Reference, ReferenceType } from './entity';
import { EntityManager } from './EntityManager';
import { LockMode } from './unit-of-work';
import { Platform } from './platforms';
import { EntitySchema, MetadataStorage } from './metadata';
import { Type } from './types';
import { EventType } from './events';

export type Constructor<T> = new (...args: any[]) => T;
export type Dictionary<T = any> = { [k: string]: T };
Expand Down Expand Up @@ -152,8 +153,6 @@ export interface EntityProperty<T extends AnyEntity<T> = any> {
referencedTableName: string;
}

export type HookType = 'onInit' | 'beforeCreate' | 'afterCreate' | 'beforeUpdate' | 'afterUpdate' | 'beforeDelete' | 'afterDelete';

export interface EntityMetadata<T extends AnyEntity<T> = any> {
name: string;
className: string;
Expand All @@ -176,7 +175,7 @@ export interface EntityMetadata<T extends AnyEntity<T> = any> {
indexes: { properties: string | string[]; name?: string; type?: string; options?: Dictionary }[];
uniques: { properties: string | string[]; name?: string; options?: Dictionary }[];
customRepository: () => Constructor<EntityRepository<T>>;
hooks: Partial<Record<HookType, (string & keyof T)[]>>;
hooks: Partial<Record<keyof typeof EventType, (string & keyof T)[]>>;
prototype: T;
class: Constructor<T>;
abstract: boolean;
Expand Down