replace custom HTTP client with n8n's requestWithAuthentication helper (#2635)

This commit is contained in:
Prakash Maheshwaran
2025-06-09 05:15:35 -04:00
committed by GitHub
parent 9fd8680c83
commit f121ce0ce6

View File

@@ -1,77 +1,46 @@
import { FieldType, IDataObject, IExecuteSingleFunctions, IHttpRequestMethods, IHttpRequestOptions, ILoadOptionsFunctions, INodePropertyOptions, INodeType, INodeTypeDescription, NodeConnectionType, ResourceMapperField, ResourceMapperFields } from 'n8n-workflow'; import {
import https from 'https'; FieldType,
import http from 'http'; IDataObject,
import { URL } from 'url'; IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
ResourceMapperField,
ResourceMapperFields,
} from 'n8n-workflow';
async function makeRequest(url: string, options: any = {}): Promise<any> { async function skyvernApiRequest(
return new Promise((resolve, reject) => { this: IExecuteSingleFunctions | ILoadOptionsFunctions,
const parsedUrl = new URL(url); method: IHttpRequestMethods,
const transport = parsedUrl.protocol === 'https:' ? https : http; endpoint: string,
const requestOptions = { body: IDataObject | undefined = undefined,
hostname: parsedUrl.hostname, ): Promise<any> {
path: parsedUrl.pathname + parsedUrl.search, const credentials = await this.getCredentials('skyvernApi');
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), const options: IHttpRequestOptions = {
method: options.method || 'GET', baseURL: credentials.baseUrl as string,
headers: options.headers || {}, method,
}; url: endpoint,
body,
const req = transport.request(requestOptions, (res) => { json: true,
let data = ''; };
return this.helpers.requestWithAuthentication.call(this, 'skyvernApi', options);
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
const response = {
ok: true,
status: res.statusCode,
statusText: res.statusMessage || '',
headers: res.headers,
json: () => {
try {
return Promise.resolve(JSON.parse(data));
} catch (e) {
return Promise.reject(new Error('Invalid JSON response'));
}
},
text: () => Promise.resolve(data),
blob: () => Promise.resolve(new Blob([data])),
arrayBuffer: () => Promise.resolve(Buffer.from(data)),
clone: () => response,
};
resolve(response);
} else {
reject(new Error(`Request failed with status code ${res.statusCode}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
if (options.body) {
req.write(options.body);
}
req.end();
});
} }
export class Skyvern implements INodeType { export class Skyvern implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Skyvern', displayName: 'Skyvern',
name: 'skyvern', name: 'skyvern',
icon: 'file:skyvern.png', // eslint-disable-line icon: 'file:skyvern.svg',
group: ['transform'], group: ['transform'],
description: 'Node to interact with Skyvern', description: 'Node to interact with Skyvern',
defaults: { defaults: {
name: 'Skyvern', name: 'Skyvern',
}, },
inputs: [NodeConnectionType.Main], // eslint-disable-line inputs: ['main'],
outputs: [NodeConnectionType.Main], // eslint-disable-line outputs: ['main'],
credentials: [ credentials: [
{ {
name: 'skyvernApi', name: 'skyvernApi',
@@ -98,51 +67,100 @@ export class Skyvern implements INodeType {
}, },
{ {
displayName: 'Operation', displayName: 'Operation',
name: 'taskOperation', name: 'operation',
type: 'options', type: 'options',
noDataExpression: true,
required: true, required: true,
default: 'dispatch', default: 'dispatchTask',
options: [ options: [
{ {
name: 'Dispatch a Task', name: 'Dispatch a Task',
value: 'dispatch', value: 'dispatchTask',
action: 'Dispatch a task to execute asynchronously',
description: 'Dispatch a task to execute asynchronously', description: 'Dispatch a task to execute asynchronously',
displayOptions: {
show: {
resource: ['task'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: 'POST',
url: '/v1/run/tasks',
},
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const taskOptions = this.getNodeParameter('taskOptions') as IDataObject;
const legacy_engine = taskOptions['engine'] as string | null;
if (legacy_engine === 'v1') {
(requestOptions.body as IDataObject)['engine'] = 'skyvern-1.0';
} else if (legacy_engine === 'v2') {
(requestOptions.body as IDataObject)['engine'] = 'skyvern-2.0';
}
return requestOptions;
},
],
},
},
}, },
{ {
name: 'Get a Task', name: 'Get a Task',
value: 'get', value: 'getTask',
action: 'Get a task by ID',
description: 'Get a task by ID', description: 'Get a task by ID',
displayOptions: {
show: {
resource: ['task'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: 'GET',
url: '/v1/run/tasks',
},
},
},
{
name: 'Get a Workflow Run',
value: 'getWorkflow',
action: 'Get a workflow run by ID',
description: 'Get a workflow run by ID',
displayOptions: {
show: {
resource: ['workflow'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: 'GET',
},
},
},
{
name: 'Dispatch a Workflow Run',
value: 'dispatchWorkflow',
action: 'Dispatch a workflow run to execute asynchronously',
description: 'Dispatch a workflow run to execute asynchronously',
displayOptions: {
show: {
resource: ['workflow'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: 'POST',
},
},
}, },
], ],
displayOptions: {
show: {
resource: ['task'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: '={{ $value === "dispatch" ? "POST" : "GET" }}' as IHttpRequestMethods,
url: '={{"/v1/run/tasks"}}',
},
send: {
preSend: [
async function (this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions): Promise<IHttpRequestOptions> {
const taskOperation = this.getNodeParameter('taskOperation');
if (taskOperation === "get") return requestOptions;
const taskOptions: IDataObject = this.getNodeParameter('taskOptions') as IDataObject;
const legacy_engine = taskOptions["engine"] as string | null
if (legacy_engine === "v1") {
(requestOptions.body as IDataObject)['engine'] = "skyvern-1.0";
}else if (legacy_engine === "v2") {
(requestOptions.body as IDataObject)['engine'] = "skyvern-2.0";
}
return requestOptions;
},
],
},
},
}, },
{ {
displayName: 'User Prompt', displayName: 'User Prompt',
@@ -155,7 +173,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['task'], resource: ['task'],
taskOperation: ['dispatch'], operation: ['dispatchTask'],
}, },
}, },
routing: { routing: {
@@ -176,7 +194,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['task'], resource: ['task'],
taskOperation: ['dispatch'], operation: ['dispatchTask'],
}, },
}, },
routing: { routing: {
@@ -197,7 +215,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['task'], resource: ['task'],
taskOperation: ['dispatch'], operation: ['dispatchTask'],
}, },
}, },
routing: { routing: {
@@ -218,7 +236,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['task'], resource: ['task'],
taskOperation: ['get'], operation: ['getTask'],
}, },
}, },
routing: { routing: {
@@ -292,12 +310,12 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['task'], resource: ['task'],
taskOperation: ['dispatch'], operation: ['dispatchTask'],
}, },
}, },
}, },
{ {
displayName: 'Workflow Title or ID', // eslint-disable-line displayName: 'Workflow Name or ID',
description: 'The title of the workflow. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.', description: 'The title of the workflow. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
name: 'workflowId', name: 'workflowId',
type: 'options', type: 'options',
@@ -313,36 +331,6 @@ export class Skyvern implements INodeType {
}, },
}, },
}, },
{
displayName: 'Workflow Operation',
name: 'workflowOperation',
type: 'options',
required: true,
default: 'get',
options: [
{
name: 'Get a Workflow Run',
value: 'get',
description: 'Get a workflow run by ID',
},
{
name: 'Dispatch a Workflow Run',
value: 'dispatch',
description: 'Dispatch a workflow run to execute asynchronously',
},
],
displayOptions: {
show: {
resource: ['workflow'],
},
},
routing: {
request: {
baseURL: '={{$credentials.baseUrl}}',
method: '={{ $value === "dispatch" ? "POST" : "GET" }}' as IHttpRequestMethods,
},
},
},
{ {
displayName: 'Workflow Run ID', displayName: 'Workflow Run ID',
description: 'The ID of the workflow run', description: 'The ID of the workflow run',
@@ -353,7 +341,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['workflow'], resource: ['workflow'],
workflowOperation: ['get'], operation: ['getWorkflow'],
}, },
}, },
routing: { routing: {
@@ -376,7 +364,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['workflow'], resource: ['workflow'],
workflowOperation: ['dispatch'], operation: ['dispatchWorkflow'],
}, },
}, },
typeOptions: { typeOptions: {
@@ -411,7 +399,7 @@ export class Skyvern implements INodeType {
displayOptions: { displayOptions: {
show: { show: {
resource: ['workflow'], resource: ['workflow'],
workflowOperation: ['dispatch'], operation: ['dispatchWorkflow'],
}, },
}, },
routing: { routing: {
@@ -432,16 +420,12 @@ export class Skyvern implements INodeType {
const resource = this.getCurrentNodeParameter('resource') as string; const resource = this.getCurrentNodeParameter('resource') as string;
if (resource !== 'workflow') return []; if (resource !== 'workflow') return [];
const credentials = await this.getCredentials('skyvernApi'); const response = await skyvernApiRequest.call(
const response = await makeRequest(credentials['baseUrl'] + '/api/v1/workflows?page_size=100', { this,
headers: { 'GET',
'x-api-key': credentials['apiKey'], '/api/v1/workflows?page_size=100',
}, );
}); const data = response;
if (!response.ok) {
throw new Error('Request to get workflows failed'); // eslint-disable-line
}
const data = await response.json();
return data.map((workflow: any) => ({ return data.map((workflow: any) => ({
name: workflow.title, name: workflow.title,
value: workflow.workflow_permanent_id, value: workflow.workflow_permanent_id,
@@ -453,22 +437,17 @@ export class Skyvern implements INodeType {
const resource = this.getCurrentNodeParameter('resource') as string; const resource = this.getCurrentNodeParameter('resource') as string;
if (resource !== 'workflow') return { fields: [] }; if (resource !== 'workflow') return { fields: [] };
const workflowOperation = this.getCurrentNodeParameter('workflowOperation') as string; const operation = this.getCurrentNodeParameter('operation') as string;
if (workflowOperation !== 'dispatch') return { fields: [] }; if (operation !== 'dispatchWorkflow') return { fields: [] };
const workflowId = this.getCurrentNodeParameter('workflowId') as string; const workflowId = this.getCurrentNodeParameter('workflowId') as string;
if (!workflowId) return { fields: [] }; if (!workflowId) return { fields: [] };
const credentials = await this.getCredentials('skyvernApi'); const workflow = await skyvernApiRequest.call(
const response = await makeRequest(credentials['baseUrl'] + '/api/v1/workflows/' + workflowId, { this,
headers: { 'GET',
'x-api-key': credentials['apiKey'], `/api/v1/workflows/${workflowId}`,
}, );
});
if (!response.ok) {
throw new Error('Request to get workflow failed'); // eslint-disable-line
}
const workflow = await response.json();
const parameters: any[] = workflow.workflow_definition.parameters; const parameters: any[] = workflow.workflow_definition.parameters;
const fields: ResourceMapperField[] = await Promise.all( const fields: ResourceMapperField[] = await Promise.all(
@@ -478,15 +457,11 @@ export class Skyvern implements INodeType {
let options: INodePropertyOptions[] | undefined = undefined; let options: INodePropertyOptions[] | undefined = undefined;
let parameterType: FieldType | undefined = undefined; let parameterType: FieldType | undefined = undefined;
if (parameter.parameter_type === 'credential') { if (parameter.parameter_type === 'credential') {
const credResponse = await makeRequest(credentials['baseUrl'] + '/api/v1/credentials', { const credData = await skyvernApiRequest.call(
headers: { this,
'x-api-key': credentials['apiKey'], 'GET',
}, '/api/v1/credentials',
}); );
if (!credResponse.ok) {
throw new Error('Request to get credentials failed'); // eslint-disable-line
}
const credData = await credResponse.json();
options = credData.map((credential: any) => ({ options = credData.map((credential: any) => ({
name: credential.name, name: credential.name,
value: credential.credential_id, value: credential.credential_id,