diff --git a/packages/nodes-base/credentials/WorkableApi.credentials.ts b/packages/nodes-base/credentials/WorkableApi.credentials.ts new file mode 100644 index 0000000000..4666b0945c --- /dev/null +++ b/packages/nodes-base/credentials/WorkableApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class WorkableApi implements ICredentialType { + name = 'workableApi'; + displayName = 'Workable API'; + documentationUrl = 'workable'; + properties: INodeProperties[] = [ + { + displayName: 'Subdomain', + name: 'subdomain', + type: 'string', + default: '', + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string', + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Workable/GenericFunctions.ts b/packages/nodes-base/nodes/Workable/GenericFunctions.ts new file mode 100644 index 0000000000..3f2829cfa9 --- /dev/null +++ b/packages/nodes-base/nodes/Workable/GenericFunctions.ts @@ -0,0 +1,37 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, +} from 'n8n-workflow'; + +export async function workableApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = await this.getCredentials('workableApi') as { accessToken: string, subdomain: string }; + + let options: OptionsWithUri = { + headers: { 'Authorization': `Bearer ${credentials.accessToken}` }, + method, + qs, + body, + uri: uri || `https://${credentials.subdomain}.workable.com/spi/v3${resource}`, + json: true, + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/Workable/WorkableTrigger.node.ts b/packages/nodes-base/nodes/Workable/WorkableTrigger.node.ts new file mode 100644 index 0000000000..56cfbe64d8 --- /dev/null +++ b/packages/nodes-base/nodes/Workable/WorkableTrigger.node.ts @@ -0,0 +1,201 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + workableApiRequest, +} from './GenericFunctions'; + +import { + snakeCase, +} from 'change-case'; + +export class WorkableTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Workable Trigger', + name: 'workableTrigger', + icon: 'file:workable.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["triggerOn"]}}', + description: 'Starts the workflow when Workable events occur', + defaults: { + name: 'Workable Trigger', + color: '#29b6f6', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'workableApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Trigger On', + name: 'triggerOn', + type: 'options', + options: [ + { + name: 'Candidate Created', + value: 'candidateCreated', + }, + { + name: 'Candidate Moved', + value: 'candidateMoved', + }, + ], + default: '', + required: true, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + options: [ + { + displayName: 'Job', + name: 'job', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getJobs', + }, + default: '', + description: `Get notifications only for one job`, + }, + { + displayName: 'Stage', + name: 'stage', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getStages', + }, + default: '', + description: `Get notifications for specific stages. e.g. 'hired'`, + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getJobs(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { jobs } = await workableApiRequest.call(this, 'GET', '/jobs'); + for (const job of jobs) { + returnData.push({ + name: job.full_title, + value: job.shortcode, + }); + } + return returnData; + }, + async getStages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { stages } = await workableApiRequest.call(this, 'GET', '/stages'); + for (const stage of stages) { + returnData.push({ + name: stage.name, + value: stage.slug, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const { subscriptions } = await workableApiRequest.call(this, 'GET', `/subscriptions`); + for (const subscription of subscriptions) { + if (subscription.target === webhookUrl) { + webhookData.webhookId = subscription.id as string; + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const credentials = await this.getCredentials('workableApi') as { accessToken: string, subdomain: string }; + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const triggerOn = this.getNodeParameter('triggerOn') as string; + const { stage, job } = this.getNodeParameter('filters') as IDataObject; + const endpoint = '/subscriptions'; + + const body: IDataObject = { + event: snakeCase(triggerOn).toLowerCase(), + args: { + account_id: credentials.subdomain, + ...(job) && { job_shortcode: job }, + ...(stage) && { stage_slug: stage }, + }, + target: webhookUrl, + }; + + const responseData = await workableApiRequest.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'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/subscriptions/${webhookData.webhookId}`; + try { + await workableApiRequest.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 bodyData = this.getBodyData(); + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Workable/workable.png b/packages/nodes-base/nodes/Workable/workable.png new file mode 100644 index 0000000000..f1c650bdd9 Binary files /dev/null and b/packages/nodes-base/nodes/Workable/workable.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index cc685e21f2..aec2ce0852 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -299,6 +299,7 @@ "dist/credentials/WiseApi.credentials.js", "dist/credentials/WooCommerceApi.credentials.js", "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/WorkableApi.credentials.js", "dist/credentials/WufooApi.credentials.js", "dist/credentials/XeroOAuth2Api.credentials.js", "dist/credentials/YourlsApi.credentials.js", @@ -636,6 +637,7 @@ "dist/nodes/WooCommerce/WooCommerce.node.js", "dist/nodes/WooCommerce/WooCommerceTrigger.node.js", "dist/nodes/Wordpress/Wordpress.node.js", + "dist/nodes/Workable/WorkableTrigger.node.js", "dist/nodes/WorkflowTrigger/WorkflowTrigger.node.js", "dist/nodes/WriteBinaryFile/WriteBinaryFile.node.js", "dist/nodes/Wufoo/WufooTrigger.node.js",