|
@@ -2,54 +2,178 @@
|
|
|
|
|
|
import { IInitializable } from '../interfaces/IInitializable';
|
|
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} initializeMethodKey
|
|
|
|
- * @returns {any}
|
|
|
|
|
|
+ * @param {string} initializeMethodName
|
|
|
|
+ * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
|
|
*/
|
|
*/
|
|
export function initializable (
|
|
export function initializable (
|
|
- initializeMethodKey: string = 'initialize'
|
|
|
|
|
|
+ initializeMethodName: string = 'initialize'
|
|
): (target: IInitializable, propertyKey: string | symbol) => any {
|
|
): (target: IInitializable, propertyKey: string | symbol) => any {
|
|
const decoratorName: string = Object.keys(this)[0];
|
|
const decoratorName: string = Object.keys(this)[0];
|
|
|
|
|
|
return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
|
|
return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
|
|
- const descriptor: PropertyDescriptor = {
|
|
|
|
- configurable: true,
|
|
|
|
- enumerable: true
|
|
|
|
- };
|
|
|
|
- const initializeMethod: Function = target[initializeMethodKey];
|
|
|
|
|
|
+ const initializeMethod: Function = target[initializeMethodName];
|
|
|
|
|
|
if (!initializeMethod || typeof initializeMethod !== 'function') {
|
|
if (!initializeMethod || typeof initializeMethod !== 'function') {
|
|
- throw new Error(`\`${initializeMethodKey}\` method with initialization logic not ` +
|
|
|
|
- `found. \`@${decoratorName}\` decorator requires \`${initializeMethodKey}\` method`);
|
|
|
|
|
|
+ throw new Error(`\`${initializeMethodName}\` method with initialization logic not ` +
|
|
|
|
+ `found. \`@${decoratorName}\` decorator requires \`${initializeMethodName}\` method`);
|
|
}
|
|
}
|
|
|
|
|
|
- const metadataPropertyKey: string = `_${propertyKey}`;
|
|
|
|
- const propertyDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, metadataPropertyKey) || descriptor;
|
|
|
|
- const methodDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, initializeMethodKey) || descriptor;
|
|
|
|
|
|
+ /**
|
|
|
|
+ * 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;
|
|
const originalMethod: Function = methodDescriptor.value;
|
|
|
|
|
|
- Object.defineProperty(target, propertyKey, {
|
|
|
|
- ...propertyDescriptor,
|
|
|
|
- get: function (): any {
|
|
|
|
- if (this[metadataPropertyKey] === undefined) {
|
|
|
|
- throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
|
|
|
|
|
|
+ Object.defineProperty(target, propertyName, {
|
|
|
|
+ ...methodDescriptor,
|
|
|
|
+ value: function (): void {
|
|
|
|
+ if (!Reflect.getMetadata(initializedTargetMetadataKey, this)) {
|
|
|
|
+ throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
|
|
}
|
|
}
|
|
|
|
|
|
- return this[metadataPropertyKey];
|
|
|
|
- },
|
|
|
|
- set: function (newVal: any): void {
|
|
|
|
- this[metadataPropertyKey] = newVal;
|
|
|
|
|
|
+ return originalMethod.apply(this, arguments);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
- Object.defineProperty(target, initializeMethodKey, {
|
|
|
|
- ...methodDescriptor,
|
|
|
|
- value: function (): void {
|
|
|
|
- originalMethod.apply(this, arguments);
|
|
|
|
|
|
|
|
- if (this[propertyKey]) {}
|
|
|
|
|
|
+ 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 propertyDescriptor;
|
|
|
|
- };
|
|
|
|
|
|
+ return this[initializablePropertyMetadataKey];
|
|
|
|
+ },
|
|
|
|
+ set: function (newVal: any): void {
|
|
|
|
+ this[initializablePropertyMetadataKey] = newVal;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return propertyDescriptor;
|
|
}
|
|
}
|