Wednesday, May 19, 2021

Custom decorator in typescript & how to mock them using Jest

 Let's see the basic decorator pattern in Javascript using the Typescript based library called kaop-ts.

Here are the sections for current article:

  1. What is Decorator
  2. Types of Decorator
  3. Creating the function with our custom made function decorator
1. What is Decorator?
    Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. And these decorators wrap the context and gives you a way to process the context, add something extra in the context and even change the context. The word context means, that in case of a function decorator you can have the access to the Class to which the function belongs,  the passed parameters of the actual function call and many more.

2. Types of Decorators?
 
We have many different type of decorators. Some of them are:
  • Class Decorators
  • Function Decorators
  • Property Decorator
  • Accessor Decorator
And we have some others too but the above ones are most useful as well as popular. For further details on the above mentioned types please have a look at  TypeScript Decorators.

In a nutshell, decorators are special kind of functions that return another special kind of functions which is called as a wrapped up contextual function to the actual context on which the decorator function is applied.

Sounds a bit over complex but is pretty simple to understand. Lets look at the below code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}

function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}

class ExampleClass {
  @first()
  @second()
  method() {
    console.log("Original Method");
  }
}

const exampleClass = new ExampleClass();
exampleClass.method() 

If you check out the function first & second are the decorator factories who are returning a special function and this returned function is called as a contextual wrapped function for the specific context on which first and second is applied, so in our case the console will look like:

1
2
3
4
5
first(): factory evaluated
second(): factory evaluated
first(): called
second(): called
Original Method

So, if we see the logs, we get the understanding that the factories are called and the actual function (returns of factories) of these factories gets attached to the contextual block which in our case is a method of ExampleClass.

Now lets prepare a basic log based function decorator which will log the arguments before entering the function call as well as after completing the function execution. Here is main code of the LogMe decorator and we will then see the executional part.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { applyDecorators, Logger } from '@nestjs/common';
import { afterMethod, beforeMethod } from 'kaop-ts';
import { from } from 'rxjs';
import to from 'await-to-js';

const logger = new Logger('LogMe', true);

const tc = async <T>(p: Promise<T>, errorExt?: any): Promise<[Error, T]> =>
  to(p, errorExt);

class LogMeRequestDTO {
  logArgumentsBefore?: boolean = false;
  logResult?: boolean = false;
  logArgumentsAfter?: boolean = false;
}

export const LogMe = (options: LogMeRequestDTO = {}) => {
  const before = beforeMethod((meta) => {
    /**
     * 
     */
    const printArray: string[] = [
      'ENTER LOGME',
      meta.target.constructor.name,
      meta.method.name,
    ];

    if (options?.logArgumentsBefore && meta?.args?.length > 0)
      printArray.push(JSON.stringify(meta.args));
    logger.debug(printArray.join(' -> '));
  });

  const after = afterMethod(async (meta) => {
    /**
     * 
     */

    const printArray: string[] = [
      'EXIT LOGME',
      meta.target.constructor.name,
      meta.method.name,
    ];
    let err;
    let finalResult;

    if (options?.logArgumentsAfter && meta?.args?.length > 0)
      printArray.push(JSON.stringify(meta.args));
    if (options?.logResult) {
      [err, finalResult] = await tc(from(meta.result).toPromise());
      if (err) printArray.push(JSON.stringify(err.message));
      else printArray.push(JSON.stringify(finalResult));
    }
    if (err) logger.error(printArray.join(' -> '));
    else logger.debug(printArray.join(' -> '));
  });

  return applyDecorators(before, after);
};

For now just concentrate on line 18, 33, and 57. 

Line 18: The function that will be called before the actual contextual block on which LogMe is applied
Line 33: The function that will be called after the actual contextual block on which LogMe is applied
Line 57: This is a utility function from nestjs that clubs all the decorator factories and lets you apply all of them with 1 clubbed name in our case its LogMe.

Line 11: is basically a DTO of passing the arguments for the behavior of out Logger, like whether to log anything before the execution of the contextual block or not and all. This DTO has 3 options where we can have logArgumentsBefore, logArgumentsAfter and logResult. All 3 have the exact functionality as their name suggests like:

logArgumentsBefore: Whether to log the passed arguments to contextual block, before the execution of the contextual block, 
logArgumentsAfter: Whether to log the passed arguments to contextual block, after the completion of the execution of the contextual block, 
logResult: Whether to log the result being returned from the function or not.

Now lets see how we will use the above Decorator. Since its a function decorator we will use above the functions, and as expected you will get the callback in the before function and after function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export class AppService {

  @LogMe({
    logArgumentsAfter: true,
    logArgumentsBefore: true,
    logResult: true
  })
  getHello(name: string): Promise<string> | string {
    return Promise.resolve(`Hello ${name}`);
  }
}


Line 2: shows us how to use the LogMe decorator with the DTO we defined.

When we call the function getHello of the AppService Class we will get the call in LogMe definition first (in before and after one by one) then the call is transferred to the actual call.

1
2
3
4
...
const appService = new AppService();
appService.getHello('CODEPOOL');
...

Output:

1
2
3
4
[Nest] 11020   - 05/19/2021, 9:42:55 PM   [NestFactory] Starting Nest application...
[Nest] 11020   - 05/19/2021, 9:42:55 PM   [InstanceLoader] AppModule dependencies initialized +29ms
[Nest] 11020   - 05/19/2021, 9:42:55 PM   [LogMe] ENTER LOGME -> AppService -> getHello -> ["ANKUR"] +3ms
[Nest] 11020   - 05/19/2021, 9:42:55 PM   [LogMe] EXIT LOGME -> AppService -> getHello -> ["ANKUR"] -> "Hello ANKUR" +3ms

Here is the project code Github Link

NOTE: 
In this example although we used the NEST Framework, but its not a compulsion to use, we can use any framework but it should have the support of Decorators of Typescript and if you want to use  Javascript you need to use the Babel Transpiler.

Happy Coding :)

No comments:

Post a Comment