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.
export type AopClassDecoratorOptions<T> = {
excludeMethodNames?: string[];
omitParentClass?: boolean;
} & T;
// 상속관계의 조상 클래스에 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);
};
};
/**
* 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
Overview
It would be great if
Class Decoratorfor 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.
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