Initializable.ts 6.4 KB

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