Initializable.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /* tslint:disable:no-invalid-this */
  2. import { IInitializable } from '../interfaces/IInitializable';
  3. const defaultDescriptor: PropertyDescriptor = {
  4. configurable: true,
  5. enumerable: true
  6. };
  7. const initializedTargetMetadataKey: string = '_initialized';
  8. const initializablePropertiesSetMetadataKey: string = '_initializablePropertiesSet';
  9. const wrappedMethodsSetMetadataKey: string = '_wrappedMethodsSet';
  10. const constructorMethodName: string = 'constructor';
  11. /**
  12. * @param {string} initializeMethodName
  13. * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
  14. */
  15. export function initializable (
  16. initializeMethodName: string = 'initialize'
  17. ): (target: IInitializable, propertyKey: string | symbol) => any {
  18. const decoratorName: string = Object.keys(this)[0];
  19. return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
  20. const initializeMethod: Function = target[initializeMethodName];
  21. if (!initializeMethod || typeof initializeMethod !== 'function') {
  22. throw new Error(`\`${initializeMethodName}\` method with initialization logic not ` +
  23. `found. \`@${decoratorName}\` decorator requires \`${initializeMethodName}\` method`);
  24. }
  25. /**
  26. * Stage #1: initialize target metadata
  27. */
  28. initializeTargetMetadata(initializedTargetMetadataKey, false, target);
  29. initializeTargetMetadata(initializablePropertiesSetMetadataKey, new Set(), target);
  30. initializeTargetMetadata(wrappedMethodsSetMetadataKey, new Set(), target);
  31. /**
  32. * Stage #2: wrap target methods
  33. */
  34. wrapTargetMethodsInInitializedCheck(target, initializeMethodName);
  35. wrapInitializeMethodInInitializeCheck(target, initializeMethodName, propertyKey);
  36. /**
  37. * Stage #3: wrap target properties
  38. */
  39. return wrapInitializableProperty(target, propertyKey);
  40. };
  41. }
  42. /**
  43. * @param {string} metadataKey
  44. * @param metadataValue
  45. * @param {IInitializable} target
  46. */
  47. function initializeTargetMetadata (metadataKey: string, metadataValue: any, target: IInitializable): void {
  48. const hasInitializedMetadata: boolean = Reflect.hasMetadata(metadataKey, target);
  49. if (!hasInitializedMetadata) {
  50. Reflect.defineMetadata(metadataKey, metadataValue, target);
  51. }
  52. }
  53. /**
  54. * Wraps all target methods with additional logic that check that this methods will called after `initialize` method
  55. *
  56. * @param {IInitializable} target
  57. * @param {string} initializeMethodName
  58. */
  59. function wrapTargetMethodsInInitializedCheck (target: IInitializable, initializeMethodName: string): void {
  60. const ownPropertyNames: string[] = Object.getOwnPropertyNames(target);
  61. const prohibitedPropertyNames: string[] = [initializeMethodName, constructorMethodName];
  62. ownPropertyNames.forEach((propertyName: string) => {
  63. const initializablePropertiesSet: Set <string | symbol> = Reflect
  64. .getMetadata(initializablePropertiesSetMetadataKey, target);
  65. const wrappedMethodsSet: Set <string | symbol> = Reflect
  66. .getMetadata(wrappedMethodsSetMetadataKey, target);
  67. const isProhibitedPropertyName: boolean = prohibitedPropertyNames.includes(propertyName)
  68. || initializablePropertiesSet.has(propertyName)
  69. || wrappedMethodsSet.has(propertyName);
  70. if (isProhibitedPropertyName) {
  71. return;
  72. }
  73. const targetProperty: any = target[propertyName];
  74. if (typeof targetProperty !== 'function') {
  75. return;
  76. }
  77. const methodDescriptor: PropertyDescriptor = Object
  78. .getOwnPropertyDescriptor(target, propertyName) || defaultDescriptor;
  79. const originalMethod: Function = methodDescriptor.value;
  80. Object.defineProperty(target, propertyName, {
  81. ...methodDescriptor,
  82. value: function (): void {
  83. if (!Reflect.getMetadata(initializedTargetMetadataKey, this)) {
  84. throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
  85. }
  86. return originalMethod.apply(this, arguments);
  87. }
  88. });
  89. wrappedMethodsSet.add(propertyName);
  90. });
  91. }
  92. /**
  93. * Wraps `initialize` method with additional logic to check that `initialized` properties will set
  94. *
  95. * @param {IInitializable} target
  96. * @param {string} initializeMethodName
  97. * @param {string | symbol} propertyKey
  98. */
  99. function wrapInitializeMethodInInitializeCheck (
  100. target: IInitializable,
  101. initializeMethodName: string,
  102. propertyKey: string | symbol
  103. ): void {
  104. const methodDescriptor: PropertyDescriptor = Object
  105. .getOwnPropertyDescriptor(target, initializeMethodName) || defaultDescriptor;
  106. const originalMethod: Function = methodDescriptor.value;
  107. Object.defineProperty(target, initializeMethodName, {
  108. ...methodDescriptor,
  109. value: function (): void {
  110. /**
  111. * should define metadata before `initialize` method call,
  112. * because of cases when other methods will called inside `initialize` method
  113. */
  114. Reflect.defineMetadata(initializedTargetMetadataKey, true, this);
  115. const result: any = originalMethod.apply(this, arguments);
  116. if (this[propertyKey]) {}
  117. return result;
  118. }
  119. });
  120. }
  121. /**
  122. * Wraps initializable property in additional checks
  123. *
  124. * @param {IInitializable} target
  125. * @param {string | symbol} propertyKey
  126. * @returns {PropertyDescriptor}
  127. */
  128. function wrapInitializableProperty (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor {
  129. const initializablePropertiesSet: Set <string | symbol> = Reflect
  130. .getMetadata(initializablePropertiesSetMetadataKey, target);
  131. initializablePropertiesSet.add(propertyKey);
  132. const initializablePropertyMetadataKey: string = `_${propertyKey}`;
  133. const propertyDescriptor: PropertyDescriptor = Object
  134. .getOwnPropertyDescriptor(target, initializablePropertyMetadataKey) || defaultDescriptor;
  135. Object.defineProperty(target, propertyKey, {
  136. ...propertyDescriptor,
  137. get: function (): any {
  138. if (this[initializablePropertyMetadataKey] === undefined) {
  139. throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
  140. }
  141. return this[initializablePropertyMetadataKey];
  142. },
  143. set: function (newVal: any): void {
  144. this[initializablePropertyMetadataKey] = newVal;
  145. }
  146. });
  147. return propertyDescriptor;
  148. }