Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Nest.js, how to get a service instance inside a decorator?

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;
  }
};
like image 856
gremo Avatar asked Aug 30 '18 22:08

gremo


People also ask

How does decorators work in NestJS?

Here's a simple definition: An ES2016 decorator is an expression which returns a function and can take a target, name and property descriptor as arguments. You apply it by prefixing the decorator with an @ character and placing this at the very top of what you are trying to decorate.

What is service in NestJS?

Service. In enterprise applications, we follow the SOLID principle, where S stands for Single Responsibility . The controllers are responsible for accepting HTTP requests from the client and providing a response. For providing the response, you may need to connect to some external source for data.

What is IoC container in NestJS?

DI fundamentals. Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively.


3 Answers

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);
  };
}
like image 61
florian norbert bepunkt Avatar answered Oct 02 '22 01:10

florian norbert bepunkt


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).

like image 42
Buggy Avatar answered Oct 02 '22 01:10

Buggy


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

like image 26
laaksom Avatar answered Oct 02 '22 01:10

laaksom