Skip to content

Commit

Permalink
Feat() Move decorator and types to this package
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien2p committed Jan 23, 2022
1 parent 104fe46 commit 4d1cfb5
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
project: 'tsconfig.lint.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
Expand Down
Binary file modified assets/medusa-extender.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"build:declarations": "./node_modules/.bin/tsc --emitDeclarationOnly",
"build": "npm run build:ts && npm run build:declarations",
"build:doc": "./node_modules/.bin/typedoc",
"build:graph": "./node_modules/.bin/ts_dependency_graph --filter 'types.ts' --base_path ./src --max_depth 3 --start ./src/medusa.ts | dot -T jpeg > ./assets/medusa-extender.jpeg",
"build:graph": "./node_modules/.bin/ts_dependency_graph --filter 'types.ts' --base_path ./src --max_depth 4 --start ./src/medusa.ts | dot -T jpeg > ./assets/medusa-extender.jpeg",
"test": "NODE_ENV=test ./node_modules/.bin/jest"
},
"dependencies": {
Expand Down
108 changes: 108 additions & 0 deletions src/decorators/onMedusaEvent.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Constructor, MedusaServiceStatic } from '../types';
import { EntityActions, EntityEventActionOptions, MedusaEventEmittedParams } from './types';
import { medusaEventEmitter } from '../medusa-event-emitter';

export class OnMedusaEvent {
readonly #when: string;
#targetEntity: any;

constructor(when?: string) {
this.#when = when;
}

static get Before(): OnMedusaEvent {
return this.build('Before');
}

static get After(): OnMedusaEvent {
return this.build('After');
}

private static build(when: string): OnMedusaEvent {
return new OnMedusaEvent(when);
}

public InsertEvent<Entity>(entity: Constructor<Entity>): string {
return `${this.#when}Insert${entity.name}`;
}

public UpdateEvent<Entity>(entity: Constructor<Entity>): string {
return `${this.#when}Update${entity.name}`;
}

public RemoveEvent<Entity>(entity: Constructor<Entity>): string {
return `${this.#when}Remove${entity.name}`;
}

public Insert<Entity>(
entity: Constructor<Entity>,
options: EntityEventActionOptions<Entity> = { async: false }
): MethodDecorator {
return this.buildDecorator('Insert', entity, options);
}

public Update<Entity>(
entity: Constructor<Entity>,
options: EntityEventActionOptions<Entity> = { async: false }
): MethodDecorator {
return this.buildDecorator('Update', entity, options);
}

public Remove<Entity>(
entity: Constructor<Entity>,
options: EntityEventActionOptions<Entity> = { async: false }
): MethodDecorator {
return this.buildDecorator('Remove', entity, options);
}

private buildDecorator<Entity>(
action: EntityActions,
entity: Constructor<Entity>,
options: EntityEventActionOptions<Entity> = { async: false }
) {
this.#targetEntity = entity;
return OnMedusaEntityEventDecorator(`${this.#when}${action}${entity.name}`, entity, options);
}
}

/**
* Allow to decorate a class method to register it as an event handler for an entity event.
* @param eventName The event that we are listening to
* @param targetEntity The entity for which the event is triggered
* @param async Should the event be awaiting the result
* @param customMetatype The key that represent the class in the container it belongs to (Used to resolve the real instance)
*/
function OnMedusaEntityEventDecorator<TMetatype, Entity>(
eventName: string,
targetEntity: Constructor<Entity>,
{ async, customMetatype }: { async: boolean; customMetatype?: MedusaServiceStatic<TMetatype> } = {
async: false,
}
): MethodDecorator {
return (target: MedusaServiceStatic, propertyKey: string, descriptor: PropertyDescriptor): void => {
const original = descriptor.value;
descriptor.value = async function <Entity>({
values,
resolveOrReject,
}: MedusaEventEmittedParams<Entity, any>): Promise<void> {
if (!(values.event.entity instanceof targetEntity)) {
return;
}

const promise = original.apply(this, [values]);
if (async) {
return promise
.then((res: unknown) => {
return resolveOrReject(null, res);
})
.catch((err: Error) => {
return resolveOrReject(err);
});
} else {
return resolveOrReject();
}
};

medusaEventEmitter.register(eventName, propertyKey, customMetatype ?? target);
};
}
34 changes: 34 additions & 0 deletions src/decorators/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { EntityManager, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
import { MedusaServiceStatic } from '../types';

export type EntityEventActionOptions<T> = {
async: boolean;
customMetatype?: MedusaServiceStatic<T>;
};

export type EntityActions = 'Insert' | 'Update' | 'Remove';

/**
* Event types that can be emitted.
*/
export type EntityEventType<Entity, TEntityActions extends EntityActions> = TEntityActions extends 'Insert'
? InsertEvent<Entity>
: TEntityActions extends 'Update'
? UpdateEvent<Entity>
: RemoveEvent<Entity>;

/**
* The arguments expected by the {@link OnMedusaEvent} decorator.
*/
export type MedusaEventEmittedParams<Entity, TEntityActions extends EntityActions> = {
values: MedusaEventHandlerParams<Entity, TEntityActions>;
resolveOrReject: (err?: Error, res?: unknown) => void;
};

/**
* The arguments expected by the event handler.
*/
export type MedusaEventHandlerParams<Entity, TEntityActions extends EntityActions> = {
event: EntityEventType<Entity, TEntityActions>;
transactionalEntityManager?: EntityManager;
};
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ export * from './medusa';
export * from './medusa-event-emitter';
export * from './types';

export * from './decorators/onMedusaEvent.decorator';
export * from './decorators/types';

import { Utils } from './medusa-utils';
export const MedusaUtils = {
attachOrReplaceEntitySubscriber: Utils.attachOrReplaceEntitySubscriber,
Expand Down
5 changes: 4 additions & 1 deletion src/loaders/entities.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ async function overrideEntity(entity: MedusaEntityStatic): Promise<void> {
const originalEntity = await import('@medusajs/medusa/dist/models/' + fileName);
originalEntity[entity.overriddenType.name] = entity;

const preparedLog = Utils.prepareLog('MedusaLoader#entitiesLoader', `Entity overridden - ${entity.overriddenType.name}`);
const preparedLog = Utils.prepareLog(
'MedusaLoader#entitiesLoader',
`Entity overridden - ${entity.overriddenType.name}`
);
console.log(preparedLog);
}
1 change: 0 additions & 1 deletion src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './middlewares.loader';
export * from './services.loader';
export * from './routes.loader';
export * from './api.loader';
Expand Down
2 changes: 1 addition & 1 deletion src/loaders/tests/api.loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ describe('API loader', () => {
medusaApiLoader(cradle);
expect(middlewaresLoader).toHaveBeenCalled();
});
});
});
2 changes: 1 addition & 1 deletion src/loaders/tests/entities.loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ describe('Entities loader', () => {
expect(container.hasRegistration(Another.resolutionKey)).toBeTruthy();
});
});
});
});
2 changes: 1 addition & 1 deletion src/loaders/tests/repositories.loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ describe('Repositories loader', () => {
expect(container.hasRegistration(AnotherRepository.resolutionKey)).toBeTruthy();
});
});
});
});
2 changes: 1 addition & 1 deletion src/loaders/tests/utils/asArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export function asArray(resolvers) {
return {
resolve: (container, opts) => resolvers.map((r) => container.build(r, opts)),
};
}
}
134 changes: 70 additions & 64 deletions src/medusa-event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,93 @@
import { EventEmitter } from 'events';
import { AwilixContainer } from 'awilix';
import { MedusaServiceStatic } from './types';
import { MedusaServiceStatic, Metatype } from './types';

/**
* A listener descriptor.
*/
type ListenerDescriptor<T = unknown> = {
eventName: string | symbol;
propertyName: string;
metatype: MedusaServiceStatic<T>;
eventName: string | symbol;
propertyName: string;
metatype: Metatype<T>;
};

/**
* Extended event emitter to register methods that must be call when certain events are triggered and relay the handling to the API package
*/
class MedusaEventEmitter extends EventEmitter {
#listeners: Set<ListenerDescriptor> = new Set();
#listeners: Set<ListenerDescriptor> = new Set();

constructor() {
super();
}
constructor() {
super();
}

/**
* Register a new event handler.
* @param eventName The name of the event that has to be triggered
* @param propertyName The name of the class property that will handle the event
* @param metatype The object that contains the property above
*/
public register<T>(eventName: string | symbol, propertyName: string, metatype: MedusaServiceStatic<T>): void {
const descriptor = { eventName, propertyName, metatype };
if (this.#listeners.has(descriptor)) {
return;
}
this.#listeners.add(descriptor);
}
/**
* Register a new event handler.
* @param eventName The name of the event that has to be triggered
* @param propertyName The name of the class property that will handle the event
* @param metatype The object that contains the property above
*/
public register<T>(eventName: string | symbol, propertyName: string, metatype: MedusaServiceStatic<T>): void {
const descriptor = { eventName, propertyName, metatype };
if (this.#listeners.has(descriptor)) {
return;
}
this.#listeners.add(descriptor);
}

/**
* Apply all event handlers hold by the `listenerDescriptor`.
* @param container The IoC container that allow to resolve instance
*/
public registerListeners(container: AwilixContainer): void {
this.unregisterListeners();
/**
* Apply all event handlers hold by the `listenerDescriptor`.
* @param container The IoC container that allow to resolve instance
*/
public registerListeners(container: AwilixContainer): void {
this.unregisterListeners();

for (const listenerDescriptor of this.#listeners.values()) {
const { eventName, metatype, propertyName } = listenerDescriptor;
const metatypeName = metatype.resolutionKey ?? metatype.name;
const formattedMetatypeName = `${
metatypeName.charAt(0).toLowerCase() + metatypeName.slice(1, metatypeName.length)
}`;
const metatypeInstance = ((container as any).has(`${formattedMetatypeName}`)
&& container.resolve(`${formattedMetatypeName}`));
this.on(eventName, metatypeInstance[propertyName].bind(metatypeInstance));
}
}
for (const listenerDescriptor of this.#listeners.values()) {
const { eventName, metatype, propertyName } = listenerDescriptor;
if ((metatype?.constructor as MedusaServiceStatic).resolutionKey) {
const name = (metatype?.constructor as MedusaServiceStatic).resolutionKey;
const metatypeInstance = (container as any).has(name) && container.resolve(name);
this.on(eventName, metatypeInstance[propertyName].bind(metatypeInstance));
} else {
const metatypeName = metatype?.constructor.name;
const formattedMetatypeName = `${
metatypeName.charAt(0).toLowerCase() + metatypeName.slice(1, metatypeName.length)
}`;
const metatypeInstance =
(container as any).has(`${formattedMetatypeName}`) && container.resolve(`${formattedMetatypeName}`);
this.on(eventName, metatypeInstance[propertyName].bind(metatypeInstance));
}
}
}

public unregisterListeners(): void {
for (const listenerDescriptor of this.#listeners.values()) {
const { eventName } = listenerDescriptor;
this.removeAllListeners(eventName);
}
}
public unregisterListeners(): void {
for (const listenerDescriptor of this.#listeners.values()) {
const { eventName } = listenerDescriptor;
this.removeAllListeners(eventName);
}
}

/**
* Emit an asynchrone event entity based and wait for the result.
* @param eventName The event that must be triggered
* @param values The data that are passed to the event handler
*/
public async emitAsync(eventName: string | symbol, values: Record<string, unknown>): Promise<any | never> {
const eventListenerCount = this.listenerCount(eventName);
if (!eventListenerCount) {
return Promise.resolve();
}
/**
* Emit an asynchrone event entity based and wait for the result.
* @param eventName The event that must be triggered
* @param values The data that are passed to the event handler
*/
public async emitAsync(eventName: string | symbol, values: Record<string, unknown>): Promise<any | never> {
const eventListenerCount = this.listenerCount(eventName);
if (!eventListenerCount) {
return Promise.resolve();
}

return new Promise((resolve, reject) => {
this.emit(eventName, {
values,
resolveOrReject: (err?: Error, res?: unknown) => {
if (err) return reject(err);
return resolve(res);
},
});
});
}
return new Promise((resolve, reject) => {
this.emit(eventName, {
values,
resolveOrReject: (err?: Error, res?: unknown) => {
if (err) return reject(err);
return resolve(res);
},
});
});
}
}

/**
Expand Down
4 changes: 1 addition & 3 deletions src/medusa-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ export class Utils {
*/
static prepareLog(context: string, description: string): string {
const date = new Date().toLocaleString('en-US', { hour12: true });
return `${chalk.blue(`[Server] -`)} ${date} ${chalk.yellow(`[${context}]`)} ${chalk.blue(
description
)}`;
return `${chalk.blue(`[Server] -`)} ${date} ${chalk.yellow(`[${context}]`)} ${chalk.blue(description)}`;
}

/**
Expand Down

0 comments on commit 4d1cfb5

Please sign in to comment.