Invoke a method within a function triggered by the .call() method

Currently, I am developing an n8n node that essentially functions every time a specific event occurs.

To facilitate this process, I have created an abstract class which is invoked by the n8n environment. However, there seems to be a limitation in calling its methods as n8n utilizes Class.execute.call(thisArgs), thereby overriding the context of this for the class instance.

Invocation of my class by n8n lib

The following code snippet has been extracted from the n8n source code:

import { createContext, Script } from 'vm'
import { AbstractNode } from './n8n'

const context = createContext({ require })
export const loadClassInIsolation = <T>(filePath: string, className: string) => {
  const script = new Script(`new (require('${filePath}').${className})()`)
  return script.runInContext(context) as T
}

async function run(): Promise<void> {
  const myClass = loadClassInIsolation<AbstractNode<unknown>>(
    '../dist/codex/node/Codex.node.js',
    'Codex',
  )
  const thisArgs = {
    prepareOutputData: (d: any): any => ({ ...d }),
  }
  console.log(await myClass.execute.call(thisArgs, thisArgs))
}

void run()

Overview of my abstract class

This is the class where I am encountering difficulties involving the use of this:

import { IExecuteFunctions, INodeExecutionData, INodeType } from 'n8n-workflow'

export abstract class AbstractNode<TParams> implements Omit<INodeType, 'description'> {
  private _executeFunctions: IExecuteFunctions = null

  set executeFunctions(value: IExecuteFunctions) {
    this._executeFunctions = value
  }

  get executeFunctions(): IExecuteFunctions {
    return this._executeFunctions
  }

  abstract run(t: TParams): Promise<INodeExecutionData>

  async execute(): Promise<INodeExecutionData[][]> {
    this.executeFunctions = this as unknown as IExecuteFunctions

    // THIS LINE DOES NOT WORK
    // ERROR: TypeError: this.run is not a function
    await this.run({ prompts: ['hello', 'world'] } as TParams)

    return this.executeFunctions.prepareOutputData([
      { json: { answer: 'Sample answer' } },
    ])
  }
}

Dynamic instantiation of the class

This particular class implements the abstract run method defined in the AbstractNode:

import { Logger } from '@nestjs/common'
import { FirefliesContext } from '@src/common'
import { AbstractNode } from '@src/n8n'
import { INodeExecutionData } from 'n8n-workflow'

type CodexParams = { prompts: string[] }

export class Codex extends AbstractNode<CodexParams> {
  run({ prompts }: CodexParams): Promise<INodeExecutionData> {
    console.log(`Prompts="${prompts.join(', ')}"`)
  }
}

Attempted solutions

The issue arises due to the fact that the use of .call(thisArgs) alters the context within the execute function. One potential solution involves converting execute into an arrow function but this would result in losing access to thisArgs.

Hence, my inquiry is: Is there a feasible approach to retaining access to both the class instance this and thisArgs when utilizing .call()? Having access to both entities would enable me to effectively call the implemented abstract method and utilize helper functions from thisArgs.

Answer №1

If you want a quick and slightly messy solution, one way is to define the function inside the constructor where both values of this are accessible.

export abstract class AbstractNode<TParams> implements Omit<INodeType, 'description'> {
  execute: () => Promise<INodeExecutionData[][]>

  constructor() {
    const self = this
    this.execute = async function(this: IExecuteFunctions) {
      self.executeFunctions = this
      await self.run({ prompts: ['hello', 'world'] } as TParams)
  
      return self.executeFunctions.prepareOutputData([
        { json: { answer: 'Sample answer' } },
      ])
    }
  }
}

Another option is to consider using a class field where this is always accessible. This approach can help keep your constructor clean:

export abstract class AbstractNode<TParams> implements Omit<INodeType, 'description'> {
  execute = ((self) => async function (this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    self.executeFunctions = this
    await self.run({ prompts: ['hello', 'world'] } as TParams)

    return self.executeFunctions.prepareOutputData([
      { json: { answer: 'Sample answer' } },
    ])
  })(this)
}

You could also move it to a helper function to improve readability. Check out an example here:

const withCallContextAsArgument = <
  TCallContext,
  TArgs extends any[], 
  TReturnType
>(
  f: (this: null, callContext: TCallContext, ...args: TArgs) => TReturnType
) => function(this: TCallContext, ...args: TArgs) {
    return f.call(null, this, ...args)
}

export abstract class AbstractNode<TParams> implements Omit<INodeType, 'description'> {
  executeFunctions!: IExecuteFunctions
  abstract run(arg: any): Promise<void>

  execute = withCallContextAsArgument(async (thisArgs: IExecuteFunctions): Promise<INodeExecutionData[][]> => {
    this.executeFunctions = thisArgs
    await this.run({ prompts: ['hello', 'world'] } as TParams)

    return this.executeFunctions.prepareOutputData([
      { json: { answer: 'Sample answer' } },
    ])
  })
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for implementing a generic constant value in a TypeScript function

Is it permissible in TypeScript to have the following code snippet? function getFoo<P = "a"|"b">():string { // P represents a type, not an actual value! return "foo"; } getFoo<"a>">(); // no ...

Is there a way to apply a single mongoose hook to multiple methods in TypeScript?

Referencing the response on How to register same mongoose hook for multiple methods? const hooks = [ 'find', 'findOne', 'update' ]; UserSchema.pre( hooks, function( next ) { // stuff } The provided code functions well wi ...

Unique TypeScript code snippets tailored for VSCode

Is it possible to create detailed custom user snippets in VS Code for TypeScript functions such as: someArray.forEach((val: getTypeFromArrayOnTheFly){ } I was able to create a simple snippet, but I am unsure how to make it appear after typing an array na ...

A guide to successfully transferring data array values from a Parent Component to a Child Component using APIs in Angular

To transfer values of this.bookingInfo = bookings.responseObj.txnValues; array from the parent component to the bookingInfo array in my child component and then insert that data into the chartNameChartTTV.data = []; array in the child component. Here, divN ...

The "isActive" value does not share any properties with the type 'Properties<string | number, string & {}>'. This issue was encountered while using React with TypeScript

I'm attempting to include the isActive parameter inside NavLink of react-router-dom version 5, but I'm encountering two errors. The type '({ isActive }: { isActive: any; }) => { color: string; background: string; }' does not have an ...

Tips on pairing elements from a ngFor processed list with another list using ngIf

If we have a list such as the one shown below: elements = [ { id: 1, name: "one" }, { id: 3, name: "three" }, { id: 5, name: "five" }, { id: 6, name: "six" }, ]; lists = [ { id: 5, name: "five" }, { id: 9, ...

What is the best way to enhance the object type within a function parameter using TypeScript?

If I have a specified type like this: type Template = { a: string; b: string; c: string } I want to utilize it within a function, but with an additional parameter. How can I achieve this efficiently? When attempting to extend the type, TypeSc ...

Angular: bypassSecurityTrustHtml sanitizer does not render the attribute (click)

I'm facing an issue where a button I rendered is not executing the alertWindow function when clicked. Can anyone help?: app.component.ts: import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core'; import ...

Precise object mapping with Redux and Typescript

In my redux slice, I have defined a MyState interface with the following structure: interface MyState { key1: string, key2: boolean, key3: number, } There is an action named set which has this implementation: set: (state: MyState, action: PayloadAct ...

What is the best way to define the type of an object when the Key is already known?

If I have the code snippet below, how can I properly define the data object type based on the known value of data.type? In this scenario, I aim to assign a specific type to data when its type property is either "sms" or "email" const payload = '{&quo ...

Typescript: The dilemma of losing the reference to 'this'

My objective is to set a value for myImage, but the js target file does not contain myImage which leads to an error. How can I maintain the scope of this within typescript classes? I want to load an image with the Jimp library and then have a reference to ...

Having trouble consuming data from a service in Angular 6?

I'm in the process of creating a basic cache service in Angular; a service that includes a simple setter/getter function for different components to access data from. Unfortunately, when attempting to subscribe to this service to retrieve the data, t ...

Discover properties of a TypeScript class with an existing object

I am currently working on a project where I need to extract all the properties of a class from an object that is created as an instance of this class. My goal is to create a versatile admin page that can be used for any entity that is associated with it. ...

What is the true function of the `as` keyword within a mapped type?

I am new to typescript and I find the usage of as confusing in the following example. type foo = "a" | "b" | 1 | 2; type bar = { [k in foo as number]: any } This example passes type checking. The resulting bar type is transformed i ...

Verifying currency in mat-input field

I need help implementing validation for inputting prices on a form. For example, if a user types in $20.0000, I want a validation message to appear marking the input as invalid. Would this type of validation require regex, and if so, how would I go about ...

Customize YouTube iframe styles in Angular 4+ with TypeScript

Has anyone been successful in overriding the style of an embedded YouTube iframe using Angular 4+ with TypeScript? I've attempted to override a CSS class of the embed iframe, but have not had any luck. Here is the URL to YouTube's stylesheet: ...

Calculate the total values across a row of a work schedule, based on specific conditions

I am facing a challenge with my work schedule data, as it is laid out horizontally. I am trying to calculate the total hours in the schedule based on various criteria such as the person's name, their available hours, shift, and the machine they are as ...

Determine the date and time based on the number of days passed

Hey there! I have a dataset structured like this: let events = { "KOTH Airship": ["EVERY 19:00"], "KOTH Castle": ["EVERY 20:00"], Totem: ["EVERY 17:00", "EVERY 23:00"], Jum ...

Make sure to send individual requests in Angular instead of sending them all at once

I am looking to adjust this function so that it sends these two file ids in separate requests: return this.upload(myForm).pipe( take(1), switchMap(res => { body.user.profilePic = res.data.profilePic; body.user.coverPic = res.data.coverPic; ...

"Mastering the art of debouncing in Angular using

I am facing an issue where, during a slow internet connection, users can press the save button multiple times resulting in saving multiple sets of data. This problem doesn't occur when working locally, but it does happen on our staging environment. E ...