From 12417ea323fd9acb2d5d67e57e2ff2bfa15272f6 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 21 Aug 2021 06:53:06 -0400 Subject: [PATCH] :sparkles: Add Form.io Trigger (#2064) * Initial commit n8n smartsheet and formio nodes 1. created smarsheet node that can add new rew to a particular smarsheet. 2. created formio trigger node which will listen to form.io form submit event and process data. * Added coloum mapping logic form.io field label to smarsheet column title. API and smartsheet sheetId configuration added to smartsheet node. * Added smartsheet api credentials and used it in smartsheet node. * Added logos to the form.io and smartsheet nodes. Removed smartsheet and form.io npm dependencies from the nodes. Added type check for the variables used. fixed tslint errors in the created nodes. * :zap: Improvements to #1943 * :zap: Improvements * :zap: Improvements * :zap: Some fixes and improvements Co-authored-by: Parthiban Chandrasekar Co-authored-by: Jan Oberhauser --- .../credentials/FormIoApi.credentials.ts | 57 +++++ .../nodes/FormIo/FormIoTrigger.node.ts | 205 ++++++++++++++++++ .../nodes/FormIo/GenericFunctions.ts | 84 +++++++ packages/nodes-base/nodes/FormIo/formio.svg | 1 + packages/nodes-base/package.json | 2 + 5 files changed, 349 insertions(+) create mode 100644 packages/nodes-base/credentials/FormIoApi.credentials.ts create mode 100644 packages/nodes-base/nodes/FormIo/FormIoTrigger.node.ts create mode 100644 packages/nodes-base/nodes/FormIo/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/FormIo/formio.svg diff --git a/packages/nodes-base/credentials/FormIoApi.credentials.ts b/packages/nodes-base/credentials/FormIoApi.credentials.ts new file mode 100644 index 0000000000..50439cd738 --- /dev/null +++ b/packages/nodes-base/credentials/FormIoApi.credentials.ts @@ -0,0 +1,57 @@ +import { + ICredentialType, + INodeProperties, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class FormIoApi implements ICredentialType { + name = 'formIoApi'; + displayName = 'Form.io API'; + properties: INodeProperties[] = [ + { + displayName: 'Environment', + name: 'environment', + type: 'options', + default: 'cloudHosted', + options: [ + { + name: 'Cloud-hosted', + value: 'cloudHosted', + }, + { + name: 'Self-hosted', + value: 'selfHosted', + }, + ], + }, + { + displayName: 'Self-hosted domain', + name: 'domain', + type: 'string', + default: '', + placeholder: 'https://www.mydomain.com', + displayOptions: { + show: { + environment: [ + 'selfHosted', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/FormIo/FormIoTrigger.node.ts b/packages/nodes-base/nodes/FormIo/FormIoTrigger.node.ts new file mode 100644 index 0000000000..30cc69e3b8 --- /dev/null +++ b/packages/nodes-base/nodes/FormIo/FormIoTrigger.node.ts @@ -0,0 +1,205 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + formIoApiRequest, +} from './GenericFunctions'; + +export class FormIoTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Form.io Trigger', + name: 'formIoTrigger', + icon: 'file:formio.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle form.io events via webhooks', + defaults: { + name: 'Form.io Trigger', + color: '#6ad7b9', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'formIoApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Project Name/ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + required: true, + default: '', + description: `Choose from the list or specify an ID. You can also specify the ID using an expression` + }, + { + displayName: 'Form Name/ID', + name: 'formId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'projectId', + ], + loadOptionsMethod: 'getForms', + }, + required: true, + default: '', + description: `Choose from the list or specify an ID. You can also specify the ID using an expression` + }, + { + displayName: 'Trigger Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'Submission Created', + value: 'create', + }, + { + name: 'Submission Updated', + value: 'update', + }, + ], + required: true, + default: '', + }, + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + default: true, + description: 'Return a simplified version of the response instead of the raw data', + }, + ], + }; + + methods = { + loadOptions: { + async getProjects(this: ILoadOptionsFunctions): Promise { + const projects = await formIoApiRequest.call(this, 'GET', '/project', {}); + const returnData: INodePropertyOptions[] = []; + for (const project of projects) { + returnData.push({ + name: project.title, + value: project._id, + }); + } + return returnData; + }, + async getForms(this: ILoadOptionsFunctions): Promise { + const projectId = this.getCurrentNodeParameter('projectId') as string; + const forms = await formIoApiRequest.call(this, 'GET', `/project/${projectId}/form`, {}); + const returnData: INodePropertyOptions[] = []; + for (const form of forms) { + returnData.push({ + name: form.title, + value: form._id, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const formId = this.getNodeParameter('formId') as string; + const projectId = this.getNodeParameter('projectId') as string; + const method = this.getNodeParameter('events') as string[]; + const actions = await formIoApiRequest.call(this, 'GET', `/project/${projectId}/form/${formId}/action`); + for (const action of actions) { + if (action.name === 'webhook') { + if (action.settings.url === webhookUrl && + // tslint:disable-next-line: no-any + (action.method.length === method.length && action.method.every((value: any) => method.includes(value)))) { + webhookData.webhookId = action._id; + return true; + } + } + } + return false; + }, + + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const formId = this.getNodeParameter('formId') as string; + const projectId = this.getNodeParameter('projectId') as string; + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const method = this.getNodeParameter('events') as string[]; + const payload = { + data: { + name: `webhook`, + title: `webhook-n8n:${webhookUrl}`, + method, + handler: [ + 'after', + ], + priority: 0, + settings: { + method: 'post', + block: false, + url: webhookUrl, + }, + condition: { + field: 'submit', + }, + }, + }; + const webhook = await formIoApiRequest.call(this, 'POST', `/project/${projectId}/form/${formId}/action`, payload); + webhookData.webhookId = webhook._id; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const formId = this.getNodeParameter('formId') as string; + const projectId = this.getNodeParameter('projectId') as string; + await formIoApiRequest.call(this, 'DELETE', `/project/${projectId}/form/${formId}/action/${webhookData.webhookId}`); + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + const simple = this.getNodeParameter('simple') as boolean; + let response = req.body.request; + if (simple === true) { + response = response.data; + } + return { + workflowData: [ + this.helpers.returnJsonArray(response), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/FormIo/GenericFunctions.ts b/packages/nodes-base/nodes/FormIo/GenericFunctions.ts new file mode 100644 index 0000000000..ae42a4f28f --- /dev/null +++ b/packages/nodes-base/nodes/FormIo/GenericFunctions.ts @@ -0,0 +1,84 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, + NodeApiError, +} from 'n8n-workflow'; + + +interface IFormIoCredentials { + environment: 'cloudHosted' | ' selfHosted'; + domain?: string; + email: string, + password: string, +} + +/** + * Method has the logic to get jwt token from Form.io + * @param this + */ +async function getToken(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, credentials: IFormIoCredentials) { + const base = credentials.domain || 'https://formio.form.io'; + const options = { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: { + data: { + email: credentials.email, + password: credentials.password, + }, + }, + uri: `${base}/user/login`, + json: true, + resolveWithFullResponse: true, + }; + + console.log('options'); + console.log(JSON.stringify(options, null, 2)); + + try { + const responseObject = await this.helpers.request!(options); + return responseObject.headers['x-jwt-token']; + } catch (error) { + throw new Error(`Authentication Failed for Form.io. Please provide valid credentails/ endpoint details`); + } +} + +/** + * Method will call register or list webhooks based on the passed method in the parameter + * @param this + * @param method + */ +export async function formIoApiRequest(this: IHookFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, endpoint: string, body = {}, qs = {}): Promise { // tslint:disable-line:no-any + + const credentials = await this.getCredentials('formIoApi') as unknown as IFormIoCredentials; + + const token = await getToken.call(this, credentials); + + const base = credentials.domain || 'https://api.form.io'; + + const options = { + headers: { + 'Content-Type': 'application/json', + 'x-jwt-token': token, + }, + method, + body, + qs, + uri: `${base}${endpoint}`, + json: true, + }; + + try { + return await this.helpers.request!.call(this, options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/FormIo/formio.svg b/packages/nodes-base/nodes/FormIo/formio.svg new file mode 100644 index 0000000000..0887bca61d --- /dev/null +++ b/packages/nodes-base/nodes/FormIo/formio.svg @@ -0,0 +1 @@ +favicon-3 \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index de42d71c59..67c7c6f966 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -90,6 +90,7 @@ "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/Ftp.credentials.js", + "dist/credentials/FormIoApi.credentials.js", "dist/credentials/GetResponseApi.credentials.js", "dist/credentials/GetResponseOAuth2Api.credentials.js", "dist/credentials/GhostAdminApi.credentials.js", @@ -382,6 +383,7 @@ "dist/nodes/Ftp.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js", "dist/nodes/FreshworksCrm/FreshworksCrm.node.js", + "dist/nodes/FormIo/FormIoTrigger.node.js", "dist/nodes/Flow/Flow.node.js", "dist/nodes/Flow/FlowTrigger.node.js", "dist/nodes/Function.node.js",