typescriptdependency-injectionnestjs

How to integrate dependency injection with custom decorators?


I'm trying to create a decorator that requires dependency injection. For example:

@Injectable()
class UserService{
  @TimeoutAndCache(1000)
  async getUser(id:string):Promise<User>{
     // Make a call to db to get all Users
  }
}

The @TimeoutAndCache returns a new promise which does the following:

  1. If a call takes longer than 1000ms, returns a rejection and when the call completes, it stores to Redis (so that it can be fetched next time).
  2. If call takes less than 1000ms, simply returns the result
export const TimeoutAndCache = function timeoutCache(ts: number, namespace) {
  return function log(
    target: object,
    propertyKey: string,
    descriptor: TypedPropertyDescriptor<any>,
  ) {
    const originalMethod = descriptor.value; // save a reference to the original method
    descriptor.value = function(...args: any[]) {
      // pre
      let timedOut = false;
      // run and store result
      const result: Promise<object> = originalMethod.apply(this, args);
      const task = new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          if (!timedOut) {
            timedOut = true;
            console.log('timed out before finishing');
            reject('timedout');
          }
        }, ts);
        result.then(res => {
          if (timedOut) {
            // store in cache
            console.log('store in cache');
          } else {
            clearTimeout(timer);
            // return the result
            resolve(res);
          }
        });
      });
      return task;
    };
    return descriptor;
  };
};

I need to inject a RedisService to save the evaluated result. One way I could inject Redis Service in to the UserService, but seems kind ugly.


Solution

  • You should consider using an Interceptor instead of a custom decorator as they run earlier in the Nest pipeline and support dependency injection by default.

    However, because you want to both pass values (for cache timeout) as well as resolve dependencies you'll have to use the mixin pattern.

    import {
      ExecutionContext,
      Injectable,
      mixin,
      NestInterceptor,
    } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { TestService } from './test/test.service';
    
    @Injectable()
    export abstract class CacheInterceptor implements NestInterceptor {
      protected abstract readonly cacheDuration: number;
    
      constructor(private readonly testService: TestService) {}
    
      intercept(
        context: ExecutionContext,
        call$: Observable<any>,
      ): Observable<any> {
        // Whatever your logic needs to be
    
        return call$;
      }
    }
    
    export const makeCacheInterceptor = (cacheDuration: number) =>
      mixin(
        // tslint:disable-next-line:max-classes-per-file
        class extends CacheInterceptor {
          protected readonly cacheDuration = cacheDuration;
        },
      );
    

    You would then be able to apply the Interceptor to your handler in a similar fashion:

    @Injectable()
    class UserService{
      @UseInterceptors(makeCacheInterceptor(1000))
      async getUser(id:string):Promise<User>{
         // Make a call to db to get all Users
      }
    }