Skip to content

Repository files navigation

lib-intercept

Library for executing reflected ClassData and allowing intercepting of a method execution and result. The interceptors can be optimized away on methods that do not need them depending on how the interceptors are implemented.

  • At startup (activation generation) - IConfiguration will be executed at Activation generation on interceptors that implement it
  • At runtime - On Before / After handlers can remove the interceptor by using methods on the activation object.

It is using the lib-reflect library in order to generate the needed information for executing a method on a class.

Installation

Standard npm package install

npm i lib-intercept

Overview

The execution will happen in an execution context. The execution of the method(s) does not throw even if an error happens. The error will be available on the context result object.

The library add's two interceptors by default

  • An interceptor that scan's the method parameters for types it can find in the inversify container
  • The method invocation is implemented as an interceptor that get's the 'IActivation' object from the context and call's the target method on an instance of the target class. The class instance is created using inversify if it is bound, meaning you can inject services on the target class.


There are several layers of interceptors that can be added.

  • At the global level in the ActivationsGenerator class. Those interceptors will execute for all methods on all registered classes.
  • At the class level, those interceptors will execute only on the methods from that class.
  • At the method level, those interceptors will execute only on the target method.

The IContext will contain information regarding the execution of the target method. Like:

  • The activation information
  • Arguments to be passed to the method (at first it's an array of 'null' with length equal to the number of arguments). It is the job of the interceptors to set values on the arguments.
  • A dictionary to store custom data on the context to be passed around.
  • The result object, it contains either the result of the executing method, or the error.
  • The inversify container used for DI in case we need something in an interceptor.
enumKeepActivation{NONE=0x00,BEFORE=0x01,AFTER=0x02}interfaceIConfigureActivation{configure(activation: IActivation): KeepActivation;}interfaceIAfterActivation{after(context: IContext): Promise<void>|void;}interfaceIBeforeActivation{before(context: IContext): Promise<boolean>|boolean;}interfaceIActivation{// The reflection information for the target classclass: ClassData;// The reflection information for the target methodmethod: MethodData;// Before execution activationsbeforeActivation: IBeforeActivation[];// After execution activationsafterActivation: IAfterActivation[];// Execute this activationexecute(ctx: IContext,onError?: ActivationErrorCallback): Promise<any>;// Remove a before interceptor from the chain of this methodremoveBeforeActivation(activation: IBeforeActivation,context: IContext): void;// Remove an after interceptor from the chain of this methodremoveAfterActivation(activation: IAfterActivation,context: IContext): void;// Any data specific for this activationdata: Record<string,any>;}interfaceIContext{// error payload as set by setErrorerror: any;// result payload as set by setSuccesspayload: any;// Counter to track the execution point in before interceptors// We need this on the context since the interceptors can remnove themselfs from the chainbeforeActivationIdx: number;// The number of before interceptors that we need to executebeforeActivationLength: number;// Execute the activation (wrapper over activation.execute(ctx))execute(): Promise<any>;// Get the activation from this execution contextgetActivation(): IActivation;// Set the payload / error and make success as true setSuccess(payload: any): boolean;// Set the error / payload = null and make success as falsesetError(error: any): boolean;// Indicate if we have an error or notisSuccess(): boolean;// Get the DI containergetContainer(): Container;// Get the arguments used to call the target methodgetArguments(): any[];// Set the arguments used to call the target methodsetArguments(args: any[]): void;// Dictionary to set custom data on this contextgetData<T>(key: string,defaultVal?: T): T;setData(key: string,data: any): void;}

Order of interceptor execution withing a group

The order of interceptor executions within an interceptor group depends on the type of interceptor

Before interceptors

The order of the before interceptors is as they are used / defined, in the example bellow the call chain will be I1 -> I2 -> I3.

 @UseBefore(I1)
@UseBefore(I2)
@UseBefore(I3)// Call chain I1 -> I2 -> I3

After interceptors

The order of the after interceptors is in reverse order of the usage , in the example bellow the call chain will be A1 -> A2 -> A3.

 @UseAfter(A3)
@UseAfter(A2)
@UseAfter(A1)// Call chain A1 -> A2 -> A3

The reason is that we can't add decorators after a method. Ideally we would have

 @UseBefore(I1)
@UseBefore(I2)publicmyMethod(){}
@UseAfter(A2)
@UseAfter(A1)

So we are left with figuring out a way to set interceptors and still know how they are executed, so the way this library does it, is basically moving the interceptors from the example above, on top of the method.

 @UseBefore(I1)
@UseBefore(I2)//public myMethod(){}
@UseAfter(A2)
@UseAfter(A1)publicmyMethod(){}// I1 -> I2 -> myMethod() -> A2 -> A1

You can mix and match the usage of the decorators, however that will make it a lot harder to understand the ordering

@UseBefore(I1)
@UseAfter(A2)
@UseBefore(I2)
@UseBefore(I3)
@UseAfter(A1)

Example

For a complex example go to lib-intercept-example

Simple

// Interceptors for method executionclassLogimplementsIBeforeActivation{publicbefore(ctx: IContext): boolean{console.log('BEFORE: ',ctx.getActivation());// Let the execution continue after this methodreturntrue;}}classPerformanceimplementsIBeforeActivation,IAfterActivation{publicbefore(ctx: IContext): boolean{console.log('Start Performance');ctx.setData('Start',process.hrtime());// Let the execution continue after this methodreturntrue;}publicafter(ctx: IContext): void{conststart=ctx.getData<[number,number]>('Start');consttime=process.hrtime(start);console.log(`Took : ${time[0]*1000+time[1]/1e6} ms`);}}// Create a test classclassTestClass{// Decorate with the logger
@UseBefore(Log)
@UseActivation(Performance)publicmyMethod(): string{return'From my handler';}}constgenerator=newActivationsGenerator();// We can register as many classes as we want, for this example we only have onegenerator.register(TestClass);// Generate the activations// We have only one class with one method however we also have a 'constructor' method// We don't need to check the class name since we only registered oneconstactivation=generator.generateActivations(container).find(a=>a.method.name==='myMethod');// Create the context of executionconstcontext=newDefaultContext(newContainer(),activation);context.execute().then(()=>{console.log('RESULT',context.payload);});

Custom Context

An example of a custom context for integrating with express and passing the request / response objects to interceptors

// Either extend or implement a custom IContextclassHttpContextextendsDefaultContext{constructor(container: Container,activation: IActivation,privatereadonly_req: express.Request,privatereadonly_res: express.Response){super(container,activation);}publicgetRequest(): express.Request{returnthis._req;}publicgetResponse(): express.Response{returnthis._res;}}...classAuthorizationInterceptor ... {// Use our HTTP Context in a custom interceptor for example to check for a tokenpublicbefore(ctx: HttpContext): boolean {if(isNil(ctx.getRequest().headers['authorization'])){returnctx.setError('Unauthorized');// Or just ** throw new Error('Unauthorized'); ** the library will take care of setting the error}// Process the header and the target methodreturntrue;}}// In the request handlerconstctx=newHttpContext(container,activation,req,res);ctx.execute().then(()=>{// This example does not check for error, it just sends the result as jsonres.json(ctx.payload);/* if (!ctx.isSuccess()) res.status(400).send(ctx.error) // Send an error  else res.json(ctx.payload) // Send the result */});

About

Library for chaining method executions together and providing a context of execution.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages