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.
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()
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' } },
])
}
}
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(', ')}"`)
}
}
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
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' } },
])
})
}