From 6222861912e639595d5c07e88e30e2c16fd2ec20 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 10 Mar 2020 12:42:57 -0400 Subject: [PATCH] :sparkles: Calendly Trigger --- .../credentials/CalendlyApi.credentials.ts | 17 ++ .../nodes/Calendly/CalendlyTrigger.node.ts | 147 ++++++++++++++++++ .../nodes/Calendly/GenericFunctions.ts | 51 ++++++ .../nodes-base/nodes/Calendly/calendly.png | Bin 0 -> 4691 bytes packages/nodes-base/package.json | 6 +- 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/credentials/CalendlyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Calendly/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Calendly/calendly.png diff --git a/packages/nodes-base/credentials/CalendlyApi.credentials.ts b/packages/nodes-base/credentials/CalendlyApi.credentials.ts new file mode 100644 index 0000000000..5a659c4a32 --- /dev/null +++ b/packages/nodes-base/credentials/CalendlyApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CalendlyApi implements ICredentialType { + name = 'calendlyApi'; + displayName = 'Calendly API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts b/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts new file mode 100644 index 0000000000..29a8033809 --- /dev/null +++ b/packages/nodes-base/nodes/Calendly/CalendlyTrigger.node.ts @@ -0,0 +1,147 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + calendlyApiRequest, +} from './GenericFunctions'; + +export class CalendlyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Calendly Trigger', + name: 'calendlyTrigger', + icon: 'file:calendly.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Calendly events occure.', + defaults: { + name: 'Calendly Trigger', + color: '#374252', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'calendlyApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'invitee.created', + value: 'invitee.created', + description: 'Receive notifications when a new Calendly event is created', + }, + { + name: 'invitee.canceled', + value: 'invitee.canceled', + description: 'Receive notifications when a Calendly event is canceled', + }, + ], + default: [], + required: true, + }, + ], + + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string; + + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/hooks'; + const { data } = await calendlyApiRequest.call(this, 'GET', endpoint, {}); + + for (const webhook of data) { + if (webhook.attributes.url === webhookUrl) { + for (const event of events) { + if (!webhook.attributes.events.includes(event)) { + return false; + } + } + } + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string; + + const endpoint = '/hooks'; + + const body = { + url: webhookUrl, + events, + }; + + const responseData = await calendlyApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + console.log(webhookData) + if (webhookData.webhookId !== undefined) { + + const endpoint = `/hooks/${webhookData.webhookId}`; + + try { + await calendlyApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Calendly/GenericFunctions.ts b/packages/nodes-base/nodes/Calendly/GenericFunctions.ts new file mode 100644 index 0000000000..0d4c229d0b --- /dev/null +++ b/packages/nodes-base/nodes/Calendly/GenericFunctions.ts @@ -0,0 +1,51 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions +} from 'n8n-workflow'; + +export async function calendlyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('calendlyApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = 'https://calendly.com/api/v1'; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'X-TOKEN': credentials.apiKey, + }, + method, + body, + qs: query, + uri: uri || `${endpoint}${resource}`, + json: true + }; + if (!Object.keys(body).length) { + delete options.form; + } + if (!Object.keys(query).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.response) { + const errorMessage = error.response.body.message || error.response.body.description || error.message; + throw new Error(`Calendly error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Calendly/calendly.png b/packages/nodes-base/nodes/Calendly/calendly.png new file mode 100644 index 0000000000000000000000000000000000000000..6527e03d94beaa3a93953c7d80dcccc26e86f961 GIT binary patch literal 4691 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