diff --git a/packages/nodes-base/credentials/CopperApi.credentials.ts b/packages/nodes-base/credentials/CopperApi.credentials.ts new file mode 100644 index 0000000000..ea3105054d --- /dev/null +++ b/packages/nodes-base/credentials/CopperApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CopperApi implements ICredentialType { + name = 'copperApi'; + displayName = 'Copper API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Email', + name: 'email', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts new file mode 100644 index 0000000000..2c95c25ebf --- /dev/null +++ b/packages/nodes-base/nodes/Copper/CopperTrigger.node.ts @@ -0,0 +1,174 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + copperApiRequest, + getAutomaticSecret, +} from './GenericFunctions'; + +export class CopperTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Copper Trigger', + name: 'copperTrigger', + icon: 'file:copper.png', + group: ['trigger'], + version: 1, + description: 'Handle Copper events via webhooks', + defaults: { + name: 'Copper Trigger', + color: '#ff2564', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'copperApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Company', + value: 'company', + }, + { + name: 'Lead', + value: 'lead', + }, + { + name: 'Opportunity', + value: 'opportunity', + }, + { + name: 'Person', + value: 'person', + }, + { + name: 'Project', + value: 'project', + }, + { + name: 'Task', + value: 'task', + }, + ], + description: 'The resource which will fire the event.', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Delete', + value: 'delete', + description: 'An existing record is removed', + }, + { + name: 'New', + value: 'new', + description: 'A new record is created', + }, + { + name: 'Update', + value: 'update', + description: 'Any field in the existing entity record is changed', + }, + ], + description: 'The event to listen to.', + }, + ], + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/webhooks/${webhookData.webhookId}`; + try { + await copperApiRequest.call(this, 'GET', endpoint); + } catch (err) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const resource = this.getNodeParameter('resource') as string; + const event = this.getNodeParameter('event') as string; + const endpoint = '/webhooks'; + const body: IDataObject = { + target: webhookUrl, + type: resource, + event, + }; + + const credentials = this.getCredentials('copperApi'); + body.secret = { + secret: getAutomaticSecret(credentials!), + }; + + const { id } = await copperApiRequest.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 copperApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const credentials = this.getCredentials('copperApi'); + const req = this.getRequestObject(); + + // Check if the supplied secret matches. If not ignore request. + if (req.body.secret !== getAutomaticSecret(credentials!)) { + return {}; + } + + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Copper/GenericFunctions.ts b/packages/nodes-base/nodes/Copper/GenericFunctions.ts new file mode 100644 index 0000000000..31bd1379c2 --- /dev/null +++ b/packages/nodes-base/nodes/Copper/GenericFunctions.ts @@ -0,0 +1,63 @@ +import { createHash } from 'crypto'; +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { + ICredentialDataDecryptedObject, + IDataObject, +} from 'n8n-workflow'; + +export async function copperApiRequest(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('copperApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'X-PW-AccessToken': credentials.apiKey, + 'X-PW-Application': 'developer_api', + 'X-PW-UserEmail': credentials.email, + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: uri ||`https://api.prosperworks.com/developer_api/v1${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) { + let errorMessage = error.message; + if (error.response.body && error.response.body.message) { + errorMessage = error.response.body.message; + } + + throw new Error('Copper Error: ' + errorMessage); + } +} + + +/** + * Creates a secret from the credentials + * + * @export + * @param {ICredentialDataDecryptedObject} credentials + * @returns + */ +export function getAutomaticSecret(credentials: ICredentialDataDecryptedObject) { + const data = `${credentials.email},${credentials.apiKey}`; + return createHash('md5').update(data).digest("hex"); +} diff --git a/packages/nodes-base/nodes/Copper/copper.png b/packages/nodes-base/nodes/Copper/copper.png new file mode 100644 index 0000000000..befa65c181 Binary files /dev/null and b/packages/nodes-base/nodes/Copper/copper.png differ diff --git a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts index 53a0a75302..709b9858cd 100644 --- a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts +++ b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts @@ -19,7 +19,7 @@ import { export class WebflowTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Webflow Trigger', - name: 'webflow', + name: 'webflowTrigger', icon: 'file:webflow.png', group: ['trigger'], version: 1, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 4afb893073..829d20caf4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -34,6 +34,7 @@ "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CodaApi.credentials.js", + "dist/credentials/CopperApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", @@ -95,6 +96,7 @@ "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/Coda/Coda.node.js", + "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Dropbox/Dropbox.node.js",