52

In CustomDecorator, how to access a service instance defined in Nest.js?

export const CustomDecorator = (): MethodDecorator => {
  return (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
    ) => {

    // Here, is possibile to access a Nest.js service (i.e. TestService) instance?

    return descriptor;
  }
};
gremo
  • 47,186
  • 75
  • 257
  • 421

4 Answers4

39

Late to the party, but since I had a similar problem (Use global nest module in decorator) and stumbled upon this question.

import { Inject } from '@nestjs/common';
export function yourDecorator() {
  const injectYourService = Inject(YourServiceClass);

  return (target: any, propertyKey: string, propertyDescriptor: PropertyDescriptor) => {
    // this is equivalent to have a constructor like constructor(yourservice: YourServiceClass)
    // note that this will injected to the instance, while your decorator runs for the class constructor
    injectYourService(target, 'yourservice');

    // do something in you decorator

    // we use a ref here so we can type it
    const yourservice: YourServiceClass = this.yourservice;
    yourservice.someMethod(someParam);
  };
}
florian norbert bepunkt
  • 2,099
  • 1
  • 21
  • 32
  • What if there is already an instance with the name ? Basically if I use decorator multiple times on same service but different functions. I tried and it doesn't throw errors but is there a side-effect. – Mohit Singh Dec 29 '22 at 06:21
22

We have a few point:

  • Property decorator executed before decorated instance will be created.
  • Decorator want to use some instance resolved by Injector of decorated instance.

As a straightforward way - use some instance injected by decorated instance.

@Injectable()
export class CatsService {
  constructor(public myService: MyService){}

  @CustomDecorator()
  foo(){}
}

export const CustomDecorator = (): MethodDecorator => {
  return (
    target: Object,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
  ) => {

    const originalMethod = descriptor.value;

    descriptor.value = function () {
      const serviceInstance = this;
      console.log(serviceInstance.myService);

    }

    return descriptor;
  }
};

PS i think it is somehow possible to use instance of Injector to get any of desired instances (like angular does).

Buggy
  • 3,539
  • 1
  • 21
  • 38
  • 6
    Good idea, byt not exactly what I want. I need service instance in `CustomDecorator`, without the need for `CatService` to inject `MyService`. That is, `MyService` should be injected only in my decorator (where I put those comments). – gremo Sep 11 '18 at 21:03
  • Good workaround, but I am struggling with this as well. Eventually I need a way to inject and `MyService` only in the decorator. – turbopasi Jan 27 '22 at 14:32
  • @buggy One question: So this `originalMethod` is actually the `foo()` where the decorator is applied? Can I able to call this originalMethod inside this decorator? Something like: 1. Do some pre-processing 2. Call the originalMethod 3. Do some book-keeping Is this possible to be in this method decorator? – Pradip Feb 09 '23 at 12:52
16

Came across this question and spent the day trying to figure out a good answer. This may not fit every use case, but I was able to copy a common pattern in Nest's core package to suit my needs.

I wanted to create my own decorator for annotating controller methods to handle events (e.g, @Subscribe('some.topic.key') async handler() { ... })).

To implement this, my decorator used SetMetadata from @nestjs/common to register some metadata I required (the method name it was being applied to, the class it belonged to, a reference to the method).

export const Subscribe = (topic: string) => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    SetMetadata<string, RabbitSubscriberMetadataConfiguration>(
      RABBITMQ_SUBSCRIBER,
      {
        topic,
        target: target.constructor.name,
        methodName: propertyKey,
        callback: descriptor.value,
      },
    )(target, propertyKey, descriptor);
  };
};

From there, I was able to create my own module which hooked into Nest's lifecycle hooks to find all methods I had decorated with my decorator, and apply some logic to it, e.g:

@Module({
  imports: [RabbitmqChannelProvider],
  providers: [RabbitmqService, MetadataScanner, RabbitmqSubscriberExplorer],
  exports: [RabbitmqService],
})
export class RabbitmqModule implements OnModuleInit {
  constructor(
    private readonly explorer: RabbitmqSubscriberExplorer,
    private readonly rabbitmqService: RabbitmqService,
  ) {}

  async onModuleInit() {
    // find everything marked with @Subscribe
    const subscribers = this.explorer.explore();
    // set up subscriptions
    for (const subscriber of subscribers) {
      await this.rabbitmqService.subscribe(
        subscriber.topic,
        subscriber.callback,
      );
    }
  }
}

The explorer service used some utilities in @nestjs/core to introspect the container and handle finding all the decorated functions with their metadata.

@Injectable()
export class RabbitmqSubscriberExplorer {
  constructor(
    private readonly modulesContainer: ModulesContainer,
    private readonly metadataScanner: MetadataScanner,
  ) {}

  public explore(): RabbitSubscriberMetadataConfiguration[] {
    // find all the controllers
    const modules = [...this.modulesContainer.values()];
    const controllersMap = modules
      .filter(({ controllers }) => controllers.size > 0)
      .map(({ controllers }) => controllers);

    // munge the instance wrappers into a nice format
    const instanceWrappers: InstanceWrapper<Controller>[] = [];
    controllersMap.forEach(map => {
      const mapKeys = [...map.keys()];
      instanceWrappers.push(
        ...mapKeys.map(key => {
          return map.get(key);
        }),
      );
    });

    // find the handlers marked with @Subscribe
    return instanceWrappers
      .map(({ instance }) => {
        const instancePrototype = Object.getPrototypeOf(instance);
        return this.metadataScanner.scanFromPrototype(
          instance,
          instancePrototype,
          method =>
            this.exploreMethodMetadata(instance, instancePrototype, method),
        );
      })
      .reduce((prev, curr) => {
        return prev.concat(curr);
      });
  }

  public exploreMethodMetadata(
    instance: object,
    instancePrototype: Controller,
    methodKey: string,
  ): RabbitSubscriberMetadataConfiguration | null {
    const targetCallback = instancePrototype[methodKey];
    const handler = Reflect.getMetadata(RABBITMQ_SUBSCRIBER, targetCallback);
    if (handler == null) {
      return null;
    }
    return handler;
  }
}

I am not espousing this as being the best way to handle this, but it has worked well for me. Use this code at your own risk, it should get you started :-). I adapted the code available from here: https://github.com/nestjs/nest/blob/5.1.0-stable/packages/microservices/listener-metadata-explorer.ts

laaksom
  • 2,050
  • 2
  • 18
  • 17
-15

I was trying to use my config service inside a ParamDecorator, so I access my service by creating a new instance of it :

export const MyParamDecorator = createParamDecorator((data, req) => {

  // ...
  const configService = new ConfigService(`${process.env.NODE_ENV || 'default'}.env`);
  const myConfigValue = configService.getMyValue();
  // ...
});