diff --git a/integrations/n8n/nodes/Skyvern/Skyvern.node.ts b/integrations/n8n/nodes/Skyvern/Skyvern.node.ts index a00d6a52..86ac604c 100644 --- a/integrations/n8n/nodes/Skyvern/Skyvern.node.ts +++ b/integrations/n8n/nodes/Skyvern/Skyvern.node.ts @@ -1,77 +1,46 @@ -import { FieldType, IDataObject, IExecuteSingleFunctions, IHttpRequestMethods, IHttpRequestOptions, ILoadOptionsFunctions, INodePropertyOptions, INodeType, INodeTypeDescription, NodeConnectionType, ResourceMapperField, ResourceMapperFields } from 'n8n-workflow'; -import https from 'https'; -import http from 'http'; -import { URL } from 'url'; +import { + FieldType, + IDataObject, + IExecuteSingleFunctions, + IHttpRequestMethods, + IHttpRequestOptions, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + ResourceMapperField, + ResourceMapperFields, +} from 'n8n-workflow'; -async function makeRequest(url: string, options: any = {}): Promise { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url); - const transport = parsedUrl.protocol === 'https:' ? https : http; - const requestOptions = { - hostname: parsedUrl.hostname, - path: parsedUrl.pathname + parsedUrl.search, - port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), - method: options.method || 'GET', - headers: options.headers || {}, - }; - - const req = transport.request(requestOptions, (res) => { - let data = ''; - - 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(); - }); +async function skyvernApiRequest( + this: IExecuteSingleFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + endpoint: string, + body: IDataObject | undefined = undefined, +): Promise { + const credentials = await this.getCredentials('skyvernApi'); + const options: IHttpRequestOptions = { + baseURL: credentials.baseUrl as string, + method, + url: endpoint, + body, + json: true, + }; + return this.helpers.requestWithAuthentication.call(this, 'skyvernApi', options); } export class Skyvern implements INodeType { description: INodeTypeDescription = { displayName: 'Skyvern', name: 'skyvern', - icon: 'file:skyvern.png', // eslint-disable-line + icon: 'file:skyvern.svg', group: ['transform'], description: 'Node to interact with Skyvern', defaults: { name: 'Skyvern', }, - inputs: [NodeConnectionType.Main], // eslint-disable-line - outputs: [NodeConnectionType.Main], // eslint-disable-line + inputs: ['main'], + outputs: ['main'], credentials: [ { name: 'skyvernApi', @@ -98,51 +67,100 @@ export class Skyvern implements INodeType { }, { displayName: 'Operation', - name: 'taskOperation', + name: 'operation', type: 'options', + noDataExpression: true, required: true, - default: 'dispatch', + default: 'dispatchTask', options: [ { name: 'Dispatch a Task', - value: 'dispatch', + value: 'dispatchTask', + action: '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 { + 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', - value: 'get', + value: 'getTask', + action: '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 { - 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', @@ -155,7 +173,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['task'], - taskOperation: ['dispatch'], + operation: ['dispatchTask'], }, }, routing: { @@ -176,7 +194,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['task'], - taskOperation: ['dispatch'], + operation: ['dispatchTask'], }, }, routing: { @@ -197,7 +215,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['task'], - taskOperation: ['dispatch'], + operation: ['dispatchTask'], }, }, routing: { @@ -218,7 +236,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['task'], - taskOperation: ['get'], + operation: ['getTask'], }, }, routing: { @@ -292,12 +310,12 @@ export class Skyvern implements INodeType { displayOptions: { show: { 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 expression.', name: 'workflowId', 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', description: 'The ID of the workflow run', @@ -353,7 +341,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['workflow'], - workflowOperation: ['get'], + operation: ['getWorkflow'], }, }, routing: { @@ -376,7 +364,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['workflow'], - workflowOperation: ['dispatch'], + operation: ['dispatchWorkflow'], }, }, typeOptions: { @@ -411,7 +399,7 @@ export class Skyvern implements INodeType { displayOptions: { show: { resource: ['workflow'], - workflowOperation: ['dispatch'], + operation: ['dispatchWorkflow'], }, }, routing: { @@ -432,16 +420,12 @@ export class Skyvern implements INodeType { const resource = this.getCurrentNodeParameter('resource') as string; if (resource !== 'workflow') return []; - const credentials = await this.getCredentials('skyvernApi'); - const response = await makeRequest(credentials['baseUrl'] + '/api/v1/workflows?page_size=100', { - headers: { - 'x-api-key': credentials['apiKey'], - }, - }); - if (!response.ok) { - throw new Error('Request to get workflows failed'); // eslint-disable-line - } - const data = await response.json(); + const response = await skyvernApiRequest.call( + this, + 'GET', + '/api/v1/workflows?page_size=100', + ); + const data = response; return data.map((workflow: any) => ({ name: workflow.title, value: workflow.workflow_permanent_id, @@ -453,22 +437,17 @@ export class Skyvern implements INodeType { const resource = this.getCurrentNodeParameter('resource') as string; if (resource !== 'workflow') return { fields: [] }; - const workflowOperation = this.getCurrentNodeParameter('workflowOperation') as string; - if (workflowOperation !== 'dispatch') return { fields: [] }; + const operation = this.getCurrentNodeParameter('operation') as string; + if (operation !== 'dispatchWorkflow') return { fields: [] }; const workflowId = this.getCurrentNodeParameter('workflowId') as string; if (!workflowId) return { fields: [] }; - const credentials = await this.getCredentials('skyvernApi'); - const response = await makeRequest(credentials['baseUrl'] + '/api/v1/workflows/' + workflowId, { - headers: { - 'x-api-key': credentials['apiKey'], - }, - }); - if (!response.ok) { - throw new Error('Request to get workflow failed'); // eslint-disable-line - } - const workflow = await response.json(); + const workflow = await skyvernApiRequest.call( + this, + 'GET', + `/api/v1/workflows/${workflowId}`, + ); const parameters: any[] = workflow.workflow_definition.parameters; const fields: ResourceMapperField[] = await Promise.all( @@ -478,15 +457,11 @@ export class Skyvern implements INodeType { let options: INodePropertyOptions[] | undefined = undefined; let parameterType: FieldType | undefined = undefined; if (parameter.parameter_type === 'credential') { - const credResponse = await makeRequest(credentials['baseUrl'] + '/api/v1/credentials', { - headers: { - 'x-api-key': credentials['apiKey'], - }, - }); - if (!credResponse.ok) { - throw new Error('Request to get credentials failed'); // eslint-disable-line - } - const credData = await credResponse.json(); + const credData = await skyvernApiRequest.call( + this, + 'GET', + '/api/v1/credentials', + ); options = credData.map((credential: any) => ({ name: credential.name, value: credential.credential_id,