123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- import { IInitializable } from '../interfaces/IInitializable';
- const decoratorName: string = 'initializable';
- const defaultDescriptor: PropertyDescriptor = {
- configurable: true,
- enumerable: true
- };
- const initializedTargetMetadataKey: string = '_initialized';
- const initializablePropertiesSetMetadataKey: string = '_initializablePropertiesSet';
- const wrappedMethodsSetMetadataKey: string = '_wrappedMethodsSet';
- const constructorMethodName: 'constructor' = 'constructor';
- const initializeMethodName: 'initialize' = 'initialize';
- /**
- * @param {string} initializeMethodName
- * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
- */
- export function initializable (): (target: IInitializable, propertyKey: string | symbol) => any {
- return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
- const initializeMethod: Function = target[initializeMethodName];
- const isInvalidInitializeMethod = !initializeMethod || typeof initializeMethod !== 'function';
- if (isInvalidInitializeMethod) {
- 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);
- wrapInitializeMethodInInitializeCheck(target, 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
- */
- function wrapTargetMethodsInInitializedCheck (target: IInitializable): void {
- const ownPropertyNames: string[] = Object.getOwnPropertyNames(target);
- const prohibitedPropertyNames: Set<string> = new Set([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.has(propertyName)
- || initializablePropertiesSet.has(propertyName)
- || wrappedMethodsSet.has(propertyName);
- if (isProhibitedPropertyName) {
- return;
- }
- const targetProperty: IInitializable[keyof IInitializable] = 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 (): 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 | symbol} propertyKey
- */
- function wrapInitializeMethodInInitializeCheck (
- target: IInitializable,
- propertyKey: string | symbol
- ): void {
- const methodDescriptor: PropertyDescriptor = Object
- .getOwnPropertyDescriptor(target, initializeMethodName) ?? defaultDescriptor;
- const originalMethod: Function = methodDescriptor.value;
- Object.defineProperty(target, initializeMethodName, {
- ...methodDescriptor,
- value: function (): typeof originalMethod {
- /**
- * 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: typeof originalMethod = 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.toString()}`;
- 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.toString()}\` is not initialized! Initialize it first!`);
- }
- return this[initializablePropertyMetadataKey];
- },
- set: function (newVal: any): void {
- this[initializablePropertyMetadataKey] = newVal;
- }
- });
- return propertyDescriptor;
- }
|