Search code examples
typescriptn8n

Call instance method inside function called from .call()


I'm writing a n8n node which is basically a function that runs every time something happens.

I created an abstract class that is called by n8n environment, but it's not possible to call its methods as n8n calls functions using Class.execute.call(thisArgs) which overrides this context for class instance.

How n8n lib calls my class

I copied this code from 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()

My abstract class

This is the class that I'm having issue using 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' } },
    ])
  }
}

Class dynamically instantiated

This class implements abstract run method in 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(', ')}"`)
  }
}

What I've tried

The reason of this error is that .call(thisArgs) overrides this context inside execute function, one possible solution is to change execute to arrow function, but when I do that I don't have access to thisArgs.

My question is: Is there any way to have access to class instance this and thisArgs from .call()? With both I can call implemented abstract method and use helpers functions from thisArgs


Solution

  • The classic quick albeit a bit dirty way is to define this function inside of the constructor, where you have access to both this values

    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' } },
          ])
        }
      }
    }
    

    Also when you define a class field, this is always available and you can do something weird like this if you don't want the constructor to get cluttered:

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

    Or move it to helper function to make it more readable

    const withCallContextAsArgument = <
      TCallContext,
      TArgs extends any[], 
      TReturnType
    >(
      // Adding "this: null" for safety, so that you have to pass an arrow function when using
      // this helper and no "this" weirdness can occur
      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' } },
        ])
      })
    }