Skip to content

[Feature]: Class Decorator support #34

Description

@ehdgus094

Overview

@Cache({
  // ...options(metadata value)
})
export class SomeService {
  some() {
    // ...
  }
}

It would be great if Class Decorator for AOP is supported like the code block above.
What the class decorator does is basically wrap all the methods of the class.

Describe the solution you'd like

I'm currently using the code below.

  • Type Declaration
export type AopClassDecoratorOptions<T> = {
  excludeMethodNames?: string[];
  omitParentClass?: boolean;
} & T;
  • createClassDecorator
// 상속관계의 조상 클래스에 AOP가 적용될 시, 중복 적용되는 것을 방지
const methodWrappedSymbol = Symbol('METHOD_WRAPPED');

export const createClassDecorator = (
  metadataKey: symbol | string,
  metadata?: AopClassDecoratorOptions<unknown>,
): ClassDecorator => {
  const aopSymbol = Symbol('AOP_DECORATOR');
  const excludeMethodNames = metadata?.excludeMethodNames ?? [];
  const omitParentClass = metadata?.omitParentClass ?? false;
  delete metadata?.excludeMethodNames;
  delete metadata?.omitParentClass;

  return (target: any) => {
    const recursive = (currentPrototype: any) => {
      const methodNames = Object.getOwnPropertyNames(currentPrototype);
      // currentPrototype이 최상위 객체인 Object일때 재귀 종료
      if (methodNames.includes('__proto__')) return;

      const decoratorStatus = Reflect.getMetadata(
        metadataKey,
        currentPrototype.constructor,
      );

      if (decoratorStatus !== methodWrappedSymbol) {
        methodNames
          .filter(
            (methodName) =>
              methodName !== 'constructor' &&
              !excludeMethodNames.includes(methodName),
          )
          .forEach((methodName) => {
            try {
              const originalFn = currentPrototype[methodName];

              // 1. Add metadata to the method
              if (!Reflect.hasMetadata(metadataKey, originalFn)) {
                Reflect.defineMetadata(metadataKey, [], originalFn);
              }
              const metadataValues = Reflect.getMetadata(
                metadataKey,
                originalFn,
              );
              metadataValues.push({ originalFn, metadata, aopSymbol });

              // 2. Wrap the method before the lazy decorator is executed
              Object.defineProperty(currentPrototype, methodName, {
                value: function (...args: unknown[]) {
                  const wrappedFn = this[aopSymbol]?.[methodName];
                  if (wrappedFn) {
                    // If there is a wrapper stored in the method, use it
                    return wrappedFn.apply(this, args);
                  }
                  // if there is no wrapper that comes out of method, call originalFn
                  return originalFn.apply(this, args);
                },
              });

              /**
               * There are codes that using `function.name`.
               * Therefore the codes below are necessary.
               *
               * ex) @nestjs/swagger
               */
              Object.defineProperty(currentPrototype[methodName], 'name', {
                value: methodName,
                writable: false,
              });
              Object.setPrototypeOf(currentPrototype[methodName], originalFn);
            } catch (_ignored) {}
          });
        Reflect.defineMetadata(
          metadataKey,
          methodWrappedSymbol,
          currentPrototype.constructor,
        );
      }
      if (!omitParentClass) {
        recursive(Object.getPrototypeOf(currentPrototype));
      }
    };
    recursive(target.prototype);
  };
};
  • createCommonDecorator
/**
 * Class 와 Method 모두에 적용 가능한 Decorator
 */
export const createCommonDecorator = (
  metadataKey: symbol | string,
  metadata?: AopClassDecoratorOptions<unknown>,
) => {
  return function (
    target: any,
    propertyKey?: string | symbol,
    descriptor?: PropertyDescriptor,
  ) {
    const isMethodDecorator = typeof target === 'object';
    if (isMethodDecorator) {
      delete metadata?.excludeMethodNames;
      delete metadata?.omitParentClass;

      return createDecorator(metadataKey, metadata)(
        target,
        propertyKey!,
        descriptor!,
      );
    }
    return createClassDecorator(metadataKey, metadata)(target);
  };
};

It would be appreciated if you could point out any side effect that could occur by using the code above.

Thank you for this awesome library!

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions