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",