diff --git a/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts new file mode 100644 index 000000000..5ee2344ef --- /dev/null +++ b/packages/nodes-base/credentials/AcuitySchedulingApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class AcuitySchedulingApi implements ICredentialType { + name = 'acuitySchedulingApi'; + displayName = 'Acuity Scheduling API'; + properties = [ + { + displayName: 'User ID', + name: 'userId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts new file mode 100644 index 000000000..2166b5cc8 --- /dev/null +++ b/packages/nodes-base/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.ts @@ -0,0 +1,163 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + acuitySchedulingApiRequest, +} from './GenericFunctions'; + +export class AcuitySchedulingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Acuity Scheduling Trigger', + name: 'acuitySchedulingTrigger', + icon: 'file:acuityScheduling.png', + group: ['trigger'], + version: 1, + description: 'Handle Acuity Scheduling events via webhooks', + defaults: { + name: 'Acuity Scheduling Trigger', + color: '#000000', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'acuitySchedulingApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'appointment.scheduled', + value: 'appointment.scheduled', + description: 'is called once when an appointment is initially booked', + }, + { + name: 'appointment.rescheduled', + value: 'appointment.rescheduled', + description: 'is called when the appointment is rescheduled to a new time', + }, + { + name: 'appointment.canceled', + value: 'appointment.canceled', + description: 'is called whenever an appointment is canceled', + }, + { + name: 'appointment.changed', + value: 'appointment.changed', + description: 'is called when the appointment is changed in any way', + }, + { + name: 'order.completed', + value: 'order.completed', + description: 'is called when an order is completed', + }, + ], + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + description: 'By default does the webhook-data only contain the ID of the object.
If this option gets activated it will resolve the data automatically.', + }, + ], + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = '/webhooks'; + const webhooks = await acuitySchedulingApiRequest.call(this, 'GET', endpoint); + if (Array.isArray(webhooks)) { + for (const webhook of webhooks) { + if (webhook.id === webhookData.webhookId) { + return true; + } + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const endpoint = '/webhooks'; + const body: IDataObject = { + target: webhookUrl, + event, + }; + const { id } = await acuitySchedulingApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = id; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await acuitySchedulingApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + + const resolveData = this.getNodeParameter('resolveData', false) as boolean; + + if (resolveData === false) { + // Return the data as it got received + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } + + // Resolve the data by requesting the information via API + const event = this.getNodeParameter('event', false) as string; + const eventType = event.split('.').shift(); + const endpoint = `/${eventType}s/${req.body.id}`; + const responseData = await acuitySchedulingApiRequest.call(this, 'GET', endpoint, {}); + + return { + workflowData: [ + this.helpers.returnJsonArray(responseData), + ], + }; + + + } +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts new file mode 100644 index 000000000..9a2d0fc59 --- /dev/null +++ b/packages/nodes-base/nodes/AcuityScheduling/GenericFunctions.ts @@ -0,0 +1,42 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('acuitySchedulingApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + auth: { + user: credentials.userId as string, + password: credentials.apiKey as string, + }, + method, + qs, + body, + uri: uri ||`https://acuityscheduling.com/api/v1${resource}`, + json: true + }; + try { + return await this.helpers.request!(options); + } catch (error) { + + let errorMessage = error.message; + if (error.response.body && error.response.body.message) { + errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`; + } + + throw new Error('Acuity Scheduling Error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png new file mode 100644 index 000000000..df087b60e Binary files /dev/null and b/packages/nodes-base/nodes/AcuityScheduling/acuityScheduling.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 829d20caf..e72da6de6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -27,6 +27,7 @@ "n8n": { "credentials": [ "dist/credentials/ActiveCampaignApi.credentials.js", + "dist/credentials/AcuitySchedulingApi.credentials.js", "dist/credentials/AirtableApi.credentials.js", "dist/credentials/Amqp.credentials.js", "dist/credentials/AsanaApi.credentials.js", @@ -86,6 +87,7 @@ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", "dist/nodes/ActiveCampaign/ActiveCampaignTrigger.node.js", "dist/nodes/Airtable/Airtable.node.js", + "dist/nodes/AcuityScheduling/AcuitySchedulingTrigger.node.js", "dist/nodes/Amqp/Amqp.node.js", "dist/nodes/Amqp/AmqpTrigger.node.js", "dist/nodes/Asana/Asana.node.js",