Call instance method inside function called from .call()

101 Views Asked by At

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

1

There are 1 best solutions below

3
On BEST ANSWER

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