123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- /* tslint:disable:no-invalid-this */
- import { IInitializable } from '../interfaces/IInitializable';
- const defaultDescriptor: PropertyDescriptor = {
- configurable: true,
- enumerable: true
- };
- const initializedTargetMetadataKey: string = '_initialized';
- const initializablePropertiesSetMetadataKey: string = '_initializablePropertiesSet';
- const wrappedMethodsSetMetadataKey: string = '_wrappedMethodsSet';
- const constructorMethodName: string = 'constructor';
- /**
- * @param {string} initializeMethodName
- * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
- */
- export function initializable (
- initializeMethodName: string = 'initialize'
- ): (target: IInitializable, propertyKey: string | symbol) => any {
- const decoratorName: string = Object.keys(this)[0];
- return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
- const initializeMethod: Function = target[initializeMethodName];
- if (!initializeMethod || typeof initializeMethod !== 'function') {
- throw new Error(`\`${initializeMethodName}\` method with initialization logic not ` +
- `found. \`@${decoratorName}\` decorator requires \`${initializeMethodName}\` method`);
- }
- /**
- * Stage #1: initialize target metadata
- */
- initializeTargetMetadata(initializedTargetMetadataKey, false, target);
- initializeTargetMetadata(initializablePropertiesSetMetadataKey, new Set(), target);
- initializeTargetMetadata(wrappedMethodsSetMetadataKey, new Set(), target);
- /**
- * Stage #2: wrap target methods
- */
- wrapTargetMethodsInInitializedCheck(target, initializeMethodName);
- wrapInitializeMethodInInitializeCheck(target, initializeMethodName, propertyKey);
- /**
- * Stage #3: wrap target properties
- */
- return wrapInitializableProperty(target, propertyKey);
- };
- }
- /**
- * @param {string} metadataKey
- * @param metadataValue
- * @param {IInitializable} target
- */
- function initializeTargetMetadata (metadataKey: string, metadataValue: any, target: IInitializable): void {
- const hasInitializedMetadata: boolean = Reflect.hasMetadata(metadataKey, target);
- if (!hasInitializedMetadata) {
- Reflect.defineMetadata(metadataKey, metadataValue, target);
- }
- }
- /**
- * Wraps all target methods with additional logic that check that this methods will called after `initialize` method
- *
- * @param {IInitializable} target
- * @param {string} initializeMethodName
- */
- function wrapTargetMethodsInInitializedCheck (target: IInitializable, initializeMethodName: string): void {
- const ownPropertyNames: string[] = Object.getOwnPropertyNames(target);
- const prohibitedPropertyNames: string[] = [initializeMethodName, constructorMethodName];
- ownPropertyNames.forEach((propertyName: string) => {
- const initializablePropertiesSet: Set <string | symbol> = Reflect
- .getMetadata(initializablePropertiesSetMetadataKey, target);
- const wrappedMethodsSet: Set <string | symbol> = Reflect
- .getMetadata(wrappedMethodsSetMetadataKey, target);
- const isProhibitedPropertyName: boolean = prohibitedPropertyNames.includes(propertyName)
- || initializablePropertiesSet.has(propertyName)
- || wrappedMethodsSet.has(propertyName);
- if (isProhibitedPropertyName) {
- return;
- }
- const targetProperty: any = target[propertyName];
- if (typeof targetProperty !== 'function') {
- return;
- }
- const methodDescriptor: PropertyDescriptor = Object
- .getOwnPropertyDescriptor(target, propertyName) || defaultDescriptor;
- const originalMethod: Function = methodDescriptor.value;
- Object.defineProperty(target, propertyName, {
- ...methodDescriptor,
- value: function (): void {
- if (!Reflect.getMetadata(initializedTargetMetadataKey, this)) {
- throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
- }
- return originalMethod.apply(this, arguments);
- }
- });
- wrappedMethodsSet.add(propertyName);
- });
- }
- /**
- * Wraps `initialize` method with additional logic to check that `initialized` properties will set
- *
- * @param {IInitializable} target
- * @param {string} initializeMethodName
- * @param {string | symbol} propertyKey
- */
- function wrapInitializeMethodInInitializeCheck (
- target: IInitializable,
- initializeMethodName: string,
- propertyKey: string | symbol
- ): void {
- const methodDescriptor: PropertyDescriptor = Object
- .getOwnPropertyDescriptor(target, initializeMethodName) || defaultDescriptor;
- const originalMethod: Function = methodDescriptor.value;
- Object.defineProperty(target, initializeMethodName, {
- ...methodDescriptor,
- value: function (): void {
- /**
- * should define metadata before `initialize` method call,
- * because of cases when other methods will called inside `initialize` method
- */
- Reflect.defineMetadata(initializedTargetMetadataKey, true, this);
- const result: any = originalMethod.apply(this, arguments);
- if (this[propertyKey]) {}
- return result;
- }
- });
- }
- /**
- * Wraps initializable property in additional checks
- *
- * @param {IInitializable} target
- * @param {string | symbol} propertyKey
- * @returns {PropertyDescriptor}
- */
- function wrapInitializableProperty (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor {
- const initializablePropertiesSet: Set <string | symbol> = Reflect
- .getMetadata(initializablePropertiesSetMetadataKey, target);
- initializablePropertiesSet.add(propertyKey);
- const initializablePropertyMetadataKey: string = `_${propertyKey}`;
- const propertyDescriptor: PropertyDescriptor = Object
- .getOwnPropertyDescriptor(target, initializablePropertyMetadataKey) || defaultDescriptor;
- Object.defineProperty(target, propertyKey, {
- ...propertyDescriptor,
- get: function (): any {
- if (this[initializablePropertyMetadataKey] === undefined) {
- throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
- }
- return this[initializablePropertyMetadataKey];
- },
- set: function (newVal: any): void {
- this[initializablePropertyMetadataKey] = newVal;
- }
- });
- return propertyDescriptor;
- }
|