diff --git a/packages/nodes-base/credentials/CalApi.credentials.ts b/packages/nodes-base/credentials/CalApi.credentials.ts new file mode 100644 index 000000000..04bf23171 --- /dev/null +++ b/packages/nodes-base/credentials/CalApi.credentials.ts @@ -0,0 +1,41 @@ +import { + IAuthenticateQueryAuth, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class CalApi implements ICredentialType { + name = 'calApi'; + displayName = 'Cal API'; + documentationUrl = 'cal'; + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + default: '', + }, + { + displayName: 'Host', + name: 'host', + type: 'string', + default: 'https://api.cal.com', + }, + ]; + + authenticate = { + type: 'queryAuth', + properties: { + key: 'apiKey', + value: '={{$credentials.apiKey}}', + }, + } as IAuthenticateQueryAuth; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.host}}', + url: '=/v1/memberships', + }, + }; +} diff --git a/packages/nodes-base/nodes/Cal/CalTrigger.node.json b/packages/nodes-base/nodes/Cal/CalTrigger.node.json new file mode 100644 index 000000000..cd3f13cfd --- /dev/null +++ b/packages/nodes-base/nodes/Cal/CalTrigger.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.calTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Productivity", + "Utility" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/cal" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.calTrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Cal/CalTrigger.node.ts b/packages/nodes-base/nodes/Cal/CalTrigger.node.ts new file mode 100644 index 000000000..9d4f1e9ec --- /dev/null +++ b/packages/nodes-base/nodes/Cal/CalTrigger.node.ts @@ -0,0 +1,206 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + calApiRequest, + sortOptionParameters, +} from './GenericFunctions'; + +export class CalTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cal Trigger', + name: 'calTrigger', + icon: 'file:cal.svg', + group: ['trigger'], + version: 1, + subtitle: '=Events: {{$parameter["events"].join(", ")}}', + description: 'Handle Cal events via webhooks', + defaults: { + name: 'Cal Trigger', + color: '#888', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'calApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'Booking Created', + value: 'BOOKING_CREATED', + description: 'Receive notifications when a new Cal event is created', + }, + { + name: 'Booking Cancelled', + value: 'BOOKING_CANCELLED', + description: 'Receive notifications when a Cal event is canceled', + }, + { + name: 'Booking Rescheduled', + value: 'BOOKING_RESCHEDULED', + description: 'Receive notifications when a Cal event is rescheduled', + }, + ], + default: [], + required: true, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'App ID', + name: 'appId', + type: 'string', + default: '', + }, + { + displayName: 'EventType Name or ID', + name: 'eventTypeId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventTypes', + }, + default: '', + }, + { + displayName: 'Payload Template', + name: 'payloadTemplate', + type: 'string', + default: '', + typeOptions: { + rows: 4, + } + } + ], + }, + ], + }; + + methods = { + loadOptions: { + async getEventTypes(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const data = await calApiRequest.call(this, 'GET', '/event-types', {}); + + for (const item of data.event_types) { + returnData.push({ + name: item.title, + value: item.id, + }); + } + + return sortOptionParameters(returnData); + }, + }, + }; + + // @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 data = await calApiRequest.call(this, 'GET', '/hooks', {}); + + for (const webhook of data.webhooks) { + if (webhook.subscriberUrl === webhookUrl) { + for (const event of events) { + if (!webhook.eventTriggers.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 subscriberUrl = this.getNodeWebhookUrl('default'); + const eventTriggers = this.getNodeParameter('events') as string; + const options = this.getNodeParameter('options'); + const active = true; + + const body = { + subscriberUrl, + eventTriggers, + active, + ...options as object + }; + + const responseData = await calApiRequest.call(this, 'POST', '/hooks', body); + + if (responseData.webhook.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.webhook.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/hooks/${webhookData.webhookId}`; + + try { + await calApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + 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 req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Cal/GenericFunctions.ts b/packages/nodes-base/nodes/Cal/GenericFunctions.ts new file mode 100644 index 000000000..0b8383601 --- /dev/null +++ b/packages/nodes-base/nodes/Cal/GenericFunctions.ts @@ -0,0 +1,49 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHttpRequestOptions, + IHttpRequestMethods, + IHookFunctions, + IWebhookFunctions, + NodeApiError, + INodePropertyOptions, +} from 'n8n-workflow'; + +export async function calApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, resource: string, body: any = {}, query: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = await this.getCredentials('calApi'); + + let options: IHttpRequestOptions = { + baseURL: credentials.host as string, + method, + body, + qs: query, + url: resource, + }; + + if (!Object.keys(query).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.httpRequestWithAuthentication.call(this, 'calApi', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export function sortOptionParameters(optionParameters: INodePropertyOptions[]): INodePropertyOptions[] { + optionParameters.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName < bName) { return -1; } + if (aName > bName) { return 1; } + return 0; + }); + + return optionParameters; +} diff --git a/packages/nodes-base/nodes/Cal/cal.svg b/packages/nodes-base/nodes/Cal/cal.svg new file mode 100644 index 000000000..c9a78d28e --- /dev/null +++ b/packages/nodes-base/nodes/Cal/cal.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ccdf6601e..9e7f1eda8 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -56,6 +56,7 @@ "dist/credentials/BoxOAuth2Api.credentials.js", "dist/credentials/BrandfetchApi.credentials.js", "dist/credentials/BubbleApi.credentials.js", + "dist/credentials/CalApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", @@ -365,6 +366,7 @@ "dist/nodes/Box/BoxTrigger.node.js", "dist/nodes/Brandfetch/Brandfetch.node.js", "dist/nodes/Bubble/Bubble.node.js", + "dist/nodes/Cal/CalTrigger.node.js", "dist/nodes/Calendly/CalendlyTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js",