From 2d1422f5de115a720c6e3cfa0cbc5a8a7e9d2e51 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 10 Dec 2021 11:42:20 -0500 Subject: [PATCH] :sparkles: Add Figma Trigger Node (#2521) * :sparkles: Figma Trigger * :zap: Improvements * :zap: Small cleanup Co-authored-by: Jan Oberhauser --- .../credentials/FigmaApi.credentials.ts | 18 ++ .../nodes/Figma/FigmaTrigger.node.ts | 183 ++++++++++++++++++ .../nodes/Figma/GenericFunctions.ts | 36 ++++ packages/nodes-base/nodes/Figma/figma.svg | 1 + packages/nodes-base/package.json | 2 + 5 files changed, 240 insertions(+) create mode 100644 packages/nodes-base/credentials/FigmaApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Figma/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Figma/figma.svg diff --git a/packages/nodes-base/credentials/FigmaApi.credentials.ts b/packages/nodes-base/credentials/FigmaApi.credentials.ts new file mode 100644 index 0000000000..b73ed28568 --- /dev/null +++ b/packages/nodes-base/credentials/FigmaApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class FigmaApi implements ICredentialType { + name = 'figmaApi'; + displayName = 'Figma API'; + documentationUrl = 'figma'; + properties: INodeProperties[] = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string', + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts b/packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts new file mode 100644 index 0000000000..f3a319da06 --- /dev/null +++ b/packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts @@ -0,0 +1,183 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + figmaApiRequest, +} from './GenericFunctions'; + +import { + snakeCase, +} from 'change-case'; + +import { + randomBytes, +} from 'crypto'; + +export class FigmaTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Figma Trigger (Beta)', + name: 'figmaTrigger', + icon: 'file:figma.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["triggerOn"]}}', + description: 'Starts the workflow when Figma events occur', + defaults: { + name: 'Figma Trigger (Beta)', + color: '#29b6f6', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'figmaApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Team ID', + name: 'teamId', + type: 'string', + required: true, + default: '', + description: 'Trigger will monitor this Figma Team for changes. Team ID can be found in the URL of a Figma Team page when viewed in a web browser: figma.com/files/team/{TEAM-ID}/', + }, + { + displayName: 'Trigger On', + name: 'triggerOn', + type: 'options', + options: [ + { + name: 'File Commented', + value: 'fileComment', + description: 'Triggers when someone comments on a file', + }, + { + name: 'File Deleted', + value: 'fileDelete', + description: 'Triggers whenever a file has been deleted. Does not trigger on all files within a folder, if the folder is deleted', + }, + { + name: 'File Updated', + value: 'fileUpdate', + description: 'Triggers whenever a file saves or is deleted. This occurs whenever a file is closed or within 30 seconds after changes have been made', + }, + { + name: 'File Version Updated', + value: 'fileVersionUpdate', + description: 'Triggers whenever a named version is created in the version history of a file', + }, + { + name: 'Library Publish', + value: 'libraryPublish', + description: 'Triggers whenever a library file is published', + }, + ], + 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 teamId = this.getNodeParameter('teamId') as string; + const triggerOn = this.getNodeParameter('triggerOn') as string; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const { webhooks } = await figmaApiRequest.call(this, 'GET', `/v2/teams/${teamId}/webhooks`); + for (const webhook of webhooks) { + if (webhook.endpoint === webhookUrl + && webhook.team_id === teamId + && webhook.event_type === snakeCase(triggerOn).toUpperCase() + && webhook.status === 'ACTIVE') { + 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 triggerOn = this.getNodeParameter('triggerOn') as string; + const teamId = this.getNodeParameter('teamId') as string; + const endpoint = '/v2/webhooks'; + + const body: IDataObject = { + event_type: snakeCase(triggerOn).toUpperCase(), + team_id: teamId, + description: `n8n-webhook:${webhookUrl}`, + endpoint: webhookUrl, + passcode: randomBytes(10).toString('hex') as string, + }; + + const responseData = await figmaApiRequest.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 = `/v2/webhooks/${webhookData.webhookId}`; + try { + await figmaApiRequest.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(); + + if (bodyData.event_type === 'PING') { + const res = this.getResponseObject(); + res.status(200).end(); + return { + noWebhookResponse: true, + }; + } + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Figma/GenericFunctions.ts b/packages/nodes-base/nodes/Figma/GenericFunctions.ts new file mode 100644 index 0000000000..48131f8c5f --- /dev/null +++ b/packages/nodes-base/nodes/Figma/GenericFunctions.ts @@ -0,0 +1,36 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, +} from 'n8n-workflow'; + +export async function figmaApiRequest(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('figmaApi') as { accessToken: string }; + + let options: OptionsWithUri = { + headers: { 'X-FIGMA-TOKEN': credentials.accessToken }, + method, + body, + uri: uri || `https://api.figma.com${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/Figma/figma.svg b/packages/nodes-base/nodes/Figma/figma.svg new file mode 100644 index 0000000000..f1a2faa12d --- /dev/null +++ b/packages/nodes-base/nodes/Figma/figma.svg @@ -0,0 +1 @@ +Figma.logoCreated using Figma \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2da78b154a..cc685e21f2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -88,6 +88,7 @@ "dist/credentials/EventbriteOAuth2Api.credentials.js", "dist/credentials/FacebookGraphApi.credentials.js", "dist/credentials/FacebookGraphAppApi.credentials.js", + "dist/credentials/FigmaApi.credentials.js", "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/FormIoApi.credentials.js", @@ -399,6 +400,7 @@ "dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js", "dist/nodes/Facebook/FacebookGraphApi.node.js", "dist/nodes/Facebook/FacebookTrigger.node.js", + "dist/nodes/Figma/FigmaTrigger.node.js", "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Flow/Flow.node.js", "dist/nodes/Flow/FlowTrigger.node.js",