From 6d8323fadea8af04483eb1a873df0cf3ccc2a891 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 7 Aug 2024 10:18:05 +0100 Subject: [PATCH] feat(Webflow Node): Update to use the v2 API (#9996) --- .../WebflowOAuth2Api.credentials.ts | 9 +- .../nodes/Webflow/GenericFunctions.ts | 75 ++++- .../nodes/Webflow/{ => V1}/ItemDescription.ts | 0 .../nodes/Webflow/V1/WebflowTriggerV1.node.ts | 209 ++++++++++++ .../nodes/Webflow/V1/WebflowV1.node.ts | 258 +++++++++++++++ .../nodes/Webflow/V2/WebflowTriggerV2.node.ts | 179 ++++++++++ .../nodes/Webflow/V2/WebflowV2.node.ts | 32 ++ .../Webflow/V2/actions/Item/Item.resource.ts | 56 ++++ .../V2/actions/Item/create.operation.ts | 139 ++++++++ .../V2/actions/Item/delete.operation.ts | 94 ++++++ .../Webflow/V2/actions/Item/get.operation.ts | 88 +++++ .../V2/actions/Item/getAll.operation.ts | 118 +++++++ .../V2/actions/Item/update.operation.ts | 148 +++++++++ .../nodes/Webflow/V2/actions/node.type.ts | 7 + .../nodes/Webflow/V2/actions/router.ts | 35 ++ .../Webflow/V2/actions/versionDescription.ts | 41 +++ .../nodes-base/nodes/Webflow/Webflow.node.ts | 306 ++---------------- .../nodes/Webflow/WebflowTrigger.node.ts | 289 ++--------------- 18 files changed, 1514 insertions(+), 569 deletions(-) rename packages/nodes-base/nodes/Webflow/{ => V1}/ItemDescription.ts (100%) create mode 100644 packages/nodes-base/nodes/Webflow/V1/WebflowTriggerV1.node.ts create mode 100644 packages/nodes-base/nodes/Webflow/V1/WebflowV1.node.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/WebflowTriggerV2.node.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/WebflowV2.node.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/Item.resource.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/create.operation.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/delete.operation.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/get.operation.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/getAll.operation.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/Item/update.operation.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/node.type.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/router.ts create mode 100644 packages/nodes-base/nodes/Webflow/V2/actions/versionDescription.ts diff --git a/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts b/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts index 09c5dd63d3..8c754995f8 100644 --- a/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/WebflowOAuth2Api.credentials.ts @@ -16,6 +16,13 @@ export class WebflowOAuth2Api implements ICredentialType { type: 'hidden', default: 'authorizationCode', }, + { + displayName: 'Legacy', + name: 'legacy', + type: 'boolean', + default: true, + description: 'If the legacy API should be used', + }, { displayName: 'Authorization URL', name: 'authUrl', @@ -34,7 +41,7 @@ export class WebflowOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden', - default: '', + default: '={{$self["legacy"] ? "" : "cms:read cms:write sites:read"}}', }, { displayName: 'Auth URI Query Parameters', diff --git a/packages/nodes-base/nodes/Webflow/GenericFunctions.ts b/packages/nodes-base/nodes/Webflow/GenericFunctions.ts index 4cbf8c2775..49d02bcf04 100644 --- a/packages/nodes-base/nodes/Webflow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Webflow/GenericFunctions.ts @@ -6,6 +6,7 @@ import type { IWebhookFunctions, IHttpRequestMethods, IRequestOptions, + INodePropertyOptions, } from 'n8n-workflow'; export async function webflowApiRequest( @@ -17,21 +18,9 @@ export async function webflowApiRequest( uri?: string, option: IDataObject = {}, ) { - const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken'); - let credentialsType = ''; - - if (authenticationMethod === 'accessToken') { - credentialsType = 'webflowApi'; - } - - if (authenticationMethod === 'oAuth2') { - credentialsType = 'webflowOAuth2Api'; - } + let credentialsType = 'webflowOAuth2Api'; let options: IRequestOptions = { - headers: { - 'accept-version': '1.0.0', - }, method, qs, body, @@ -40,6 +29,19 @@ export async function webflowApiRequest( }; options = Object.assign({}, options, option); + // Keep support for v1 node + if (this.getNode().typeVersion === 1) { + console.log('v1'); + const authenticationMethod = this.getNodeParameter('authentication', 0, 'accessToken'); + if (authenticationMethod === 'accessToken') { + credentialsType = 'webflowApi'; + } + options.headers = { 'accept-version': '1.0.0' }; + } else { + options.resolveWithFullResponse = true; + options.uri = `https://api.webflow.com/v2${resource}`; + } + if (Object.keys(options.qs as IDataObject).length === 0) { delete options.qs; } @@ -74,3 +76,50 @@ export async function webflowApiRequestAllItems( return returnData; } +// Load Options +export async function getSites(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const response = await webflowApiRequest.call(this, 'GET', '/sites'); + + console.log(response); + + const sites = response.body?.sites || response; + + for (const site of sites) { + returnData.push({ + name: site.displayName || site.name, + value: site.id || site._id, + }); + } + return returnData; +} +export async function getCollections(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const siteId = this.getCurrentNodeParameter('siteId'); + const response = await webflowApiRequest.call(this, 'GET', `/sites/${siteId}/collections`); + + const collections = response.body?.collections || response; + + for (const collection of collections) { + returnData.push({ + name: collection.displayName || collection.name, + value: collection.id || collection._id, + }); + } + return returnData; +} +export async function getFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const collectionId = this.getCurrentNodeParameter('collectionId'); + const response = await webflowApiRequest.call(this, 'GET', `/collections/${collectionId}`); + + const fields = response.body?.fields || response; + + for (const field of fields) { + returnData.push({ + name: `${field.displayName || field.name} (${field.type}) ${field.isRequired || field.required ? ' (required)' : ''}`, + value: field.slug, + }); + } + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/ItemDescription.ts b/packages/nodes-base/nodes/Webflow/V1/ItemDescription.ts similarity index 100% rename from packages/nodes-base/nodes/Webflow/ItemDescription.ts rename to packages/nodes-base/nodes/Webflow/V1/ItemDescription.ts diff --git a/packages/nodes-base/nodes/Webflow/V1/WebflowTriggerV1.node.ts b/packages/nodes-base/nodes/Webflow/V1/WebflowTriggerV1.node.ts new file mode 100644 index 0000000000..9163fd495e --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V1/WebflowTriggerV1.node.ts @@ -0,0 +1,209 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IHookFunctions, + IWebhookFunctions, + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { getSites, webflowApiRequest } from '../GenericFunctions'; + +export class WebflowTriggerV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'Webflow Trigger', + name: 'webflowTrigger', + icon: 'file:webflow.svg', + group: ['trigger'], + version: 1, + description: 'Handle Webflow events via webhooks', + defaults: { + name: 'Webflow Trigger', + }, + // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'webflowApi', + required: true, + displayOptions: { + show: { + authentication: ['accessToken'], + }, + }, + }, + { + name: 'webflowOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: ['oAuth2'], + }, + }, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + }, + { + displayName: 'Site Name or ID', + name: 'site', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getSites', + }, + description: + 'Site that will trigger the events. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + options: [ + { + name: 'Collection Item Created', + value: 'collection_item_created', + }, + { + name: 'Collection Item Deleted', + value: 'collection_item_deleted', + }, + { + name: 'Collection Item Updated', + value: 'collection_item_changed', + }, + { + name: 'Ecomm Inventory Changed', + value: 'ecomm_inventory_changed', + }, + { + name: 'Ecomm New Order', + value: 'ecomm_new_order', + }, + { + name: 'Ecomm Order Changed', + value: 'ecomm_order_changed', + }, + { + name: 'Form Submission', + value: 'form_submission', + }, + { + name: 'Site Publish', + value: 'site_publish', + }, + ], + default: 'form_submission', + }, + ], + }; + } + + methods = { + loadOptions: { + getSites, + }, + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const siteId = this.getNodeParameter('site') as string; + + const event = this.getNodeParameter('event') as string; + const registeredWebhooks = await webflowApiRequest.call( + this, + 'GET', + `/sites/${siteId}/webhooks`, + ); + + const webhooks = registeredWebhooks.body?.webhooks || registeredWebhooks; + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.triggerType === event) { + webhookData.webhookId = webhook._id; + return true; + } + } + + return false; + }, + + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const siteId = this.getNodeParameter('site') as string; + const event = this.getNodeParameter('event') as string; + const endpoint = `/sites/${siteId}/webhooks`; + const body: IDataObject = { + site_id: siteId, + triggerType: event, + url: webhookUrl, + }; + + const response = await webflowApiRequest.call(this, 'POST', endpoint, body); + const _id = response.body?._id || response._id; + webhookData.webhookId = _id; + return true; + }, + async delete(this: IHookFunctions): Promise { + let responseData; + const webhookData = this.getWorkflowStaticData('node'); + const siteId = this.getNodeParameter('site') as string; + const endpoint = `/sites/${siteId}/webhooks/${webhookData.webhookId}`; + try { + responseData = await webflowApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; + } + const deleted = responseData.body?.deleted || responseData.deleted; + if (!deleted) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])], + }; + } +} diff --git a/packages/nodes-base/nodes/Webflow/V1/WebflowV1.node.ts b/packages/nodes-base/nodes/Webflow/V1/WebflowV1.node.ts new file mode 100644 index 0000000000..d4392c2174 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V1/WebflowV1.node.ts @@ -0,0 +1,258 @@ +import type { + IExecuteFunctions, + IDataObject, + INodeTypeBaseDescription, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + webflowApiRequest, + webflowApiRequestAllItems, + getSites, + getCollections, + getFields, +} from '../GenericFunctions'; + +import { itemFields, itemOperations } from './ItemDescription'; + +export class WebflowV1 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + version: 1, + description: 'Consume the Webflow API', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'Webflow', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'webflowApi', + required: true, + displayOptions: { + show: { + authentication: ['accessToken'], + }, + }, + }, + { + name: 'webflowOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: ['oAuth2'], + }, + }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Item', + value: 'item', + }, + ], + default: 'item', + }, + ...itemOperations, + ...itemFields, + ], + }; + } + + methods = { + loadOptions: { + getSites, + getCollections, + getFields, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + let responseData; + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + if (resource === 'item') { + // ********************************************************************* + // item + // ********************************************************************* + + // https://developers.webflow.com/#item-model + + if (operation === 'create') { + // ---------------------------------- + // item: create + // ---------------------------------- + + // https://developers.webflow.com/#create-new-collection-item + + const collectionId = this.getNodeParameter('collectionId', i) as string; + + const properties = this.getNodeParameter( + 'fieldsUi.fieldValues', + i, + [], + ) as IDataObject[]; + + const live = this.getNodeParameter('live', i) as boolean; + + const fields = {} as IDataObject; + + properties.forEach((data) => (fields[data.fieldId as string] = data.fieldValue)); + + const body: IDataObject = { + fields, + }; + + responseData = await webflowApiRequest.call( + this, + 'POST', + `/collections/${collectionId}/items`, + body, + { live }, + ); + } else if (operation === 'delete') { + // ---------------------------------- + // item: delete + // ---------------------------------- + + // https://developers.webflow.com/#remove-collection-item + + const collectionId = this.getNodeParameter('collectionId', i) as string; + const itemId = this.getNodeParameter('itemId', i) as string; + responseData = await webflowApiRequest.call( + this, + 'DELETE', + `/collections/${collectionId}/items/${itemId}`, + ); + } else if (operation === 'get') { + // ---------------------------------- + // item: get + // ---------------------------------- + + // https://developers.webflow.com/#get-single-item + + const collectionId = this.getNodeParameter('collectionId', i) as string; + const itemId = this.getNodeParameter('itemId', i) as string; + responseData = await webflowApiRequest.call( + this, + 'GET', + `/collections/${collectionId}/items/${itemId}`, + ); + responseData = responseData.items; + } else if (operation === 'getAll') { + // ---------------------------------- + // item: getAll + // ---------------------------------- + + // https://developers.webflow.com/#get-all-items-for-a-collection + + const returnAll = this.getNodeParameter('returnAll', 0); + const collectionId = this.getNodeParameter('collectionId', i) as string; + const qs: IDataObject = {}; + + if (returnAll) { + responseData = await webflowApiRequestAllItems.call( + this, + 'GET', + `/collections/${collectionId}/items`, + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', 0); + responseData = await webflowApiRequest.call( + this, + 'GET', + `/collections/${collectionId}/items`, + {}, + qs, + ); + responseData = responseData.items; + } + } else if (operation === 'update') { + // ---------------------------------- + // item: update + // ---------------------------------- + + // https://developers.webflow.com/#update-collection-item + + const collectionId = this.getNodeParameter('collectionId', i) as string; + + const itemId = this.getNodeParameter('itemId', i) as string; + + const properties = this.getNodeParameter( + 'fieldsUi.fieldValues', + i, + [], + ) as IDataObject[]; + + const live = this.getNodeParameter('live', i) as boolean; + + const fields = {} as IDataObject; + + properties.forEach((data) => (fields[data.fieldId as string] = data.fieldValue)); + + const body: IDataObject = { + fields, + }; + + responseData = await webflowApiRequest.call( + this, + 'PUT', + `/collections/${collectionId}/items/${itemId}`, + body, + { live }, + ); + } + } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Webflow/V2/WebflowTriggerV2.node.ts b/packages/nodes-base/nodes/Webflow/V2/WebflowTriggerV2.node.ts new file mode 100644 index 0000000000..560f7390ab --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/WebflowTriggerV2.node.ts @@ -0,0 +1,179 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IHookFunctions, + IDataObject, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, + IWebhookFunctions, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { getSites, webflowApiRequest } from '../GenericFunctions'; + +export class WebflowTriggerV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + displayName: 'Webflow Trigger', + name: 'webflowTrigger', + icon: 'file:webflow.svg', + group: ['trigger'], + version: 2, + description: 'Handle Webflow events via webhooks', + defaults: { + name: 'Webflow Trigger', + }, + // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'webflowOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Site Name or ID', + name: 'site', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getSites', + }, + description: + 'Site that will trigger the events. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + options: [ + { + name: 'Collection Item Created', + value: 'collection_item_created', + }, + { + name: 'Collection Item Deleted', + value: 'collection_item_deleted', + }, + { + name: 'Collection Item Updated', + value: 'collection_item_changed', + }, + { + name: 'Ecomm Inventory Changed', + value: 'ecomm_inventory_changed', + }, + { + name: 'Ecomm New Order', + value: 'ecomm_new_order', + }, + { + name: 'Ecomm Order Changed', + value: 'ecomm_order_changed', + }, + { + name: 'Form Submission', + value: 'form_submission', + }, + { + name: 'Site Publish', + value: 'site_publish', + }, + ], + default: 'form_submission', + }, + ], + }; + } + + methods = { + loadOptions: { + getSites, + }, + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const siteId = this.getNodeParameter('site') as string; + + const event = this.getNodeParameter('event') as string; + const registeredWebhooks = await webflowApiRequest.call( + this, + 'GET', + `/sites/${siteId}/webhooks`, + ); + + const webhooks = registeredWebhooks.body?.webhooks || registeredWebhooks; + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && webhook.triggerType === event) { + webhookData.webhookId = webhook._id; + return true; + } + } + + return false; + }, + + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const siteId = this.getNodeParameter('site') as string; + const event = this.getNodeParameter('event') as string; + const endpoint = `/sites/${siteId}/webhooks`; + const body: IDataObject = { + site_id: siteId, + triggerType: event, + url: webhookUrl, + }; + + const response = await webflowApiRequest.call(this, 'POST', endpoint, body); + const _id = response.body?._id || response._id; + webhookData.webhookId = _id; + return true; + }, + async delete(this: IHookFunctions): Promise { + let responseData; + const webhookData = this.getWorkflowStaticData('node'); + const siteId = this.getNodeParameter('site') as string; + const endpoint = `/sites/${siteId}/webhooks/${webhookData.webhookId}`; + try { + responseData = await webflowApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; + } + const deleted = responseData.body?.deleted || responseData.deleted; + if (!deleted) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])], + }; + } +} diff --git a/packages/nodes-base/nodes/Webflow/V2/WebflowV2.node.ts b/packages/nodes-base/nodes/Webflow/V2/WebflowV2.node.ts new file mode 100644 index 0000000000..ad17ff9e05 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/WebflowV2.node.ts @@ -0,0 +1,32 @@ +import type { + IExecuteFunctions, + INodeType, + INodeTypeBaseDescription, + INodeTypeDescription, +} from 'n8n-workflow'; +import { getSites, getCollections, getFields } from '../GenericFunctions'; +import { versionDescription } from './actions/versionDescription'; +import { router } from './actions/router'; + +export class WebflowV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { + loadOptions: { + getSites, + getCollections, + getFields, + }, + }; + + async execute(this: IExecuteFunctions) { + return await router.call(this); + } +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/Item.resource.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/Item.resource.ts new file mode 100644 index 0000000000..420d2d8490 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/Item.resource.ts @@ -0,0 +1,56 @@ +import type { INodeProperties } from 'n8n-workflow'; + +import * as create from './create.operation'; +import * as deleteItem from './delete.operation'; +import * as get from './get.operation'; +import * as getAll from './getAll.operation'; +import * as update from './update.operation'; + +export { create, deleteItem, get, getAll, update }; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + default: 'get', + options: [ + { + name: 'Create', + value: 'create', + action: 'Create an item', + }, + { + name: 'Delete', + value: 'deleteItem', + action: 'Delete an item', + }, + { + name: 'Get', + value: 'get', + action: 'Get an item', + }, + { + name: 'Get Many', + value: 'getAll', + action: 'Get many items', + }, + { + name: 'Update', + value: 'update', + action: 'Update an item', + }, + ], + displayOptions: { + show: { + resource: ['item'], + }, + }, + }, + ...create.description, + ...deleteItem.description, + ...get.description, + ...getAll.description, + ...update.description, +]; diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/create.operation.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/create.operation.ts new file mode 100644 index 0000000000..8eeeb9cb66 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/create.operation.ts @@ -0,0 +1,139 @@ +import type { + IDataObject, + INodeExecutionData, + INodeProperties, + IExecuteFunctions, +} from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities'; +import { webflowApiRequest } from '../../../GenericFunctions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Site Name or ID', + name: 'siteId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: + 'ID of the site containing the collection whose items to add to. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Collection Name or ID', + name: 'collectionId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCollections', + loadOptionsDependsOn: ['siteId'], + }, + default: '', + description: + 'ID of the collection to add an item to. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Live', + name: 'live', + type: 'boolean', + required: true, + default: false, + description: 'Whether the item should be published on the live site', + }, + { + displayName: 'Fields', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name or ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getFields', + loadOptionsDependsOn: ['collectionId'], + }, + default: '', + description: + 'Field to set for the item to create. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + description: 'Value to set for the item to create', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['item'], + operation: ['create'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + let responseData; + for (let i = 0; i < items.length; i++) { + try { + const collectionId = this.getNodeParameter('collectionId', i) as string; + + const uiFields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as IDataObject[]; + + const live = this.getNodeParameter('live', i) as boolean; + + const fieldData = {} as IDataObject; + + uiFields.forEach((data) => (fieldData[data.fieldId as string] = data.fieldValue)); + + const body: IDataObject = { + fieldData, + }; + + responseData = await webflowApiRequest.call( + this, + 'POST', + `/collections/${collectionId}/items`, + body, + { live }, + ); + + const executionData = this.helpers.constructExecutionMetaData( + wrapData(responseData.body as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error } }); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/delete.operation.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/delete.operation.ts new file mode 100644 index 0000000000..632a8e6e93 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/delete.operation.ts @@ -0,0 +1,94 @@ +import type { + IDataObject, + INodeExecutionData, + INodeProperties, + IExecuteFunctions, +} from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities'; +import { webflowApiRequest } from '../../../GenericFunctions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Site Name or ID', + name: 'siteId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: + 'ID of the site containing the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Collection Name or ID', + name: 'collectionId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCollections', + loadOptionsDependsOn: ['siteId'], + }, + default: '', + description: + 'ID of the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + default: '', + description: 'ID of the item to operate on', + }, +]; + +const displayOptions = { + show: { + resource: ['item'], + operation: ['deleteItem'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const collectionId = this.getNodeParameter('collectionId', i) as string; + const itemId = this.getNodeParameter('itemId', i) as string; + let responseData = await webflowApiRequest.call( + this, + 'DELETE', + `/collections/${collectionId}/items/${itemId}`, + ); + + if (responseData.statusCode === 204) { + responseData = { success: true }; + } else { + responseData = { success: false }; + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error } }); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/get.operation.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/get.operation.ts new file mode 100644 index 0000000000..c02b3fc26e --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/get.operation.ts @@ -0,0 +1,88 @@ +import type { + IDataObject, + INodeExecutionData, + INodeProperties, + IExecuteFunctions, +} from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities'; +import { webflowApiRequest } from '../../../GenericFunctions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Site Name or ID', + name: 'siteId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: + 'ID of the site containing the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Collection Name or ID', + name: 'collectionId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCollections', + loadOptionsDependsOn: ['siteId'], + }, + default: '', + description: + 'ID of the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + default: '', + description: 'ID of the item to operate on', + }, +]; + +const displayOptions = { + show: { + resource: ['item'], + operation: ['get'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + let responseData; + for (let i = 0; i < items.length; i++) { + try { + const collectionId = this.getNodeParameter('collectionId', i) as string; + const itemId = this.getNodeParameter('itemId', i) as string; + responseData = await webflowApiRequest.call( + this, + 'GET', + `/collections/${collectionId}/items/${itemId}`, + ); + + const executionData = this.helpers.constructExecutionMetaData( + wrapData(responseData.body as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error } }); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/getAll.operation.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/getAll.operation.ts new file mode 100644 index 0000000000..6f9bd0f5b1 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/getAll.operation.ts @@ -0,0 +1,118 @@ +import type { + IDataObject, + INodeExecutionData, + INodeProperties, + IExecuteFunctions, +} from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities'; +import { webflowApiRequest, webflowApiRequestAllItems } from '../../../GenericFunctions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Site Name or ID', + name: 'siteId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: + 'ID of the site containing the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Collection Name or ID', + name: 'collectionId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCollections', + loadOptionsDependsOn: ['siteId'], + }, + default: '', + description: + 'ID of the collection whose items to operate on. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + returnAll: [false], + }, + }, + default: 100, + description: 'Max number of results to return', + }, +]; + +const displayOptions = { + show: { + resource: ['item'], + operation: ['getAll'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + let responseData; + for (let i = 0; i < items.length; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const collectionId = this.getNodeParameter('collectionId', i) as string; + const qs: IDataObject = {}; + + if (returnAll) { + responseData = await webflowApiRequestAllItems.call( + this, + 'GET', + `/collections/${collectionId}/items`, + {}, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await webflowApiRequest.call( + this, + 'GET', + `/collections/${collectionId}/items`, + {}, + qs, + ); + responseData = responseData.body.items; + } + + const executionData = this.helpers.constructExecutionMetaData( + wrapData(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error } }); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/Item/update.operation.ts b/packages/nodes-base/nodes/Webflow/V2/actions/Item/update.operation.ts new file mode 100644 index 0000000000..e4f94ad17d --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/Item/update.operation.ts @@ -0,0 +1,148 @@ +import type { + IDataObject, + INodeExecutionData, + INodeProperties, + IExecuteFunctions, +} from 'n8n-workflow'; + +import { updateDisplayOptions, wrapData } from '../../../../../utils/utilities'; +import { webflowApiRequest } from '../../../GenericFunctions'; + +const properties: INodeProperties[] = [ + { + displayName: 'Site Name or ID', + name: 'siteId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getSites', + }, + default: '', + description: + 'ID of the site containing the collection whose items to add to. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Collection Name or ID', + name: 'collectionId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCollections', + loadOptionsDependsOn: ['siteId'], + }, + default: '', + description: + 'ID of the collection to add an item to. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Item ID', + name: 'itemId', + type: 'string', + required: true, + default: '', + description: 'ID of the item to update', + }, + { + displayName: 'Live', + name: 'live', + type: 'boolean', + required: true, + default: false, + description: 'Whether the item should be published on the live site', + }, + { + displayName: 'Fields', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name or ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getFields', + loadOptionsDependsOn: ['collectionId'], + }, + default: '', + description: + 'Field to set for the item to create. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + description: 'Value to set for the item to create', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + resource: ['item'], + operation: ['update'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], +): Promise { + const returnData: INodeExecutionData[] = []; + let responseData; + for (let i = 0; i < items.length; i++) { + try { + const collectionId = this.getNodeParameter('collectionId', i) as string; + const itemId = this.getNodeParameter('itemId', i) as string; + + const uiFields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as IDataObject[]; + + const live = this.getNodeParameter('live', i) as boolean; + + const fieldData = {} as IDataObject; + + uiFields.forEach((data) => (fieldData[data.fieldId as string] = data.fieldValue)); + + const body: IDataObject = { + fieldData, + }; + + responseData = await webflowApiRequest.call( + this, + 'PATCH', + `/collections/${collectionId}/items/${itemId}`, + body, + { live }, + ); + + const executionData = this.helpers.constructExecutionMetaData( + wrapData(responseData.body as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error } }); + continue; + } + throw error; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/node.type.ts b/packages/nodes-base/nodes/Webflow/V2/actions/node.type.ts new file mode 100644 index 0000000000..150a5684b6 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/node.type.ts @@ -0,0 +1,7 @@ +import type { AllEntities } from 'n8n-workflow'; + +type NodeMap = { + item: 'create' | 'deleteItem' | 'get' | 'getAll' | 'update'; +}; + +export type WebflowType = AllEntities; diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/router.ts b/packages/nodes-base/nodes/Webflow/V2/actions/router.ts new file mode 100644 index 0000000000..371a9df3c6 --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/router.ts @@ -0,0 +1,35 @@ +import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; +import type { WebflowType } from './node.type'; + +import * as item from './Item/Item.resource'; + +export async function router(this: IExecuteFunctions): Promise { + let returnData: INodeExecutionData[] = []; + + const items = this.getInputData(); + const resource = this.getNodeParameter('resource', 0); + const operation = this.getNodeParameter('operation', 0); + + const webflowNodeData = { + resource, + operation, + } as WebflowType; + + try { + switch (webflowNodeData.resource) { + case 'item': + returnData = await item[webflowNodeData.operation].execute.call(this, items); + break; + default: + throw new NodeOperationError( + this.getNode(), + `The operation "${operation}" is not supported!`, + ); + } + } catch (error) { + throw error; + } + + return [returnData]; +} diff --git a/packages/nodes-base/nodes/Webflow/V2/actions/versionDescription.ts b/packages/nodes-base/nodes/Webflow/V2/actions/versionDescription.ts new file mode 100644 index 0000000000..1f93d9493d --- /dev/null +++ b/packages/nodes-base/nodes/Webflow/V2/actions/versionDescription.ts @@ -0,0 +1,41 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { INodeTypeDescription } from 'n8n-workflow'; + +import * as item from './Item/Item.resource'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'Webflow', + name: 'webflow', + icon: 'file:webflow.svg', + group: ['transform'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Webflow API', + version: [2], + defaults: { + name: 'Webflow', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'webflowOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Item', + value: 'item', + }, + ], + default: 'item', + }, + ...item.description, + ], +}; diff --git a/packages/nodes-base/nodes/Webflow/Webflow.node.ts b/packages/nodes-base/nodes/Webflow/Webflow.node.ts index 3f799cbf09..4239c8c810 100644 --- a/packages/nodes-base/nodes/Webflow/Webflow.node.ts +++ b/packages/nodes-base/nodes/Webflow/Webflow.node.ts @@ -1,292 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { webflowApiRequest, webflowApiRequestAllItems } from './GenericFunctions'; +import { WebflowV1 } from './V1/WebflowV1.node'; +import { WebflowV2 } from './V2/WebflowV2.node'; -import { itemFields, itemOperations } from './ItemDescription'; +export class Webflow extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Webflow', + name: 'webflow', + icon: 'file:webflow.svg', + group: ['transform'], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Webflow API', + defaultVersion: 2, + }; -export class Webflow implements INodeType { - description: INodeTypeDescription = { - displayName: 'Webflow', - name: 'webflow', - icon: 'file:webflow.svg', - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Consume the Webflow API', - defaults: { - name: 'Webflow', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'webflowApi', - required: true, - displayOptions: { - show: { - authentication: ['accessToken'], - }, - }, - }, - { - name: 'webflowOAuth2Api', - required: true, - displayOptions: { - show: { - authentication: ['oAuth2'], - }, - }, - }, - ], - properties: [ - { - displayName: 'Authentication', - name: 'authentication', - type: 'options', - options: [ - { - name: 'Access Token', - value: 'accessToken', - }, - { - name: 'OAuth2', - value: 'oAuth2', - }, - ], - default: 'accessToken', - }, - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Item', - value: 'item', - }, - ], - default: 'item', - }, - ...itemOperations, - ...itemFields, - ], - }; + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new WebflowV1(baseDescription), + 2: new WebflowV2(baseDescription), + }; - methods = { - loadOptions: { - async getSites(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const sites = await webflowApiRequest.call(this, 'GET', '/sites'); - for (const site of sites) { - returnData.push({ - name: site.name, - value: site._id, - }); - } - return returnData; - }, - async getCollections(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const siteId = this.getCurrentNodeParameter('siteId'); - const collections = await webflowApiRequest.call( - this, - 'GET', - `/sites/${siteId}/collections`, - ); - for (const collection of collections) { - returnData.push({ - name: collection.name, - value: collection._id, - }); - } - return returnData; - }, - async getFields(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const collectionId = this.getCurrentNodeParameter('collectionId'); - const { fields } = await webflowApiRequest.call( - this, - 'GET', - `/collections/${collectionId}`, - ); - for (const field of fields) { - returnData.push({ - name: `${field.name} (${field.type}) ${field.required ? ' (required)' : ''}`, - value: field.slug, - }); - } - return returnData; - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - - const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); - let responseData; - const returnData: INodeExecutionData[] = []; - - for (let i = 0; i < items.length; i++) { - try { - if (resource === 'item') { - // ********************************************************************* - // item - // ********************************************************************* - - // https://developers.webflow.com/#item-model - - if (operation === 'create') { - // ---------------------------------- - // item: create - // ---------------------------------- - - // https://developers.webflow.com/#create-new-collection-item - - const collectionId = this.getNodeParameter('collectionId', i) as string; - - const properties = this.getNodeParameter( - 'fieldsUi.fieldValues', - i, - [], - ) as IDataObject[]; - - const live = this.getNodeParameter('live', i) as boolean; - - const fields = {} as IDataObject; - - properties.forEach((data) => (fields[data.fieldId as string] = data.fieldValue)); - - const body: IDataObject = { - fields, - }; - - responseData = await webflowApiRequest.call( - this, - 'POST', - `/collections/${collectionId}/items`, - body, - { live }, - ); - } else if (operation === 'delete') { - // ---------------------------------- - // item: delete - // ---------------------------------- - - // https://developers.webflow.com/#remove-collection-item - - const collectionId = this.getNodeParameter('collectionId', i) as string; - const itemId = this.getNodeParameter('itemId', i) as string; - responseData = await webflowApiRequest.call( - this, - 'DELETE', - `/collections/${collectionId}/items/${itemId}`, - ); - } else if (operation === 'get') { - // ---------------------------------- - // item: get - // ---------------------------------- - - // https://developers.webflow.com/#get-single-item - - const collectionId = this.getNodeParameter('collectionId', i) as string; - const itemId = this.getNodeParameter('itemId', i) as string; - responseData = await webflowApiRequest.call( - this, - 'GET', - `/collections/${collectionId}/items/${itemId}`, - ); - responseData = responseData.items; - } else if (operation === 'getAll') { - // ---------------------------------- - // item: getAll - // ---------------------------------- - - // https://developers.webflow.com/#get-all-items-for-a-collection - - const returnAll = this.getNodeParameter('returnAll', 0); - const collectionId = this.getNodeParameter('collectionId', i) as string; - const qs: IDataObject = {}; - - if (returnAll) { - responseData = await webflowApiRequestAllItems.call( - this, - 'GET', - `/collections/${collectionId}/items`, - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', 0); - responseData = await webflowApiRequest.call( - this, - 'GET', - `/collections/${collectionId}/items`, - {}, - qs, - ); - responseData = responseData.items; - } - } else if (operation === 'update') { - // ---------------------------------- - // item: update - // ---------------------------------- - - // https://developers.webflow.com/#update-collection-item - - const collectionId = this.getNodeParameter('collectionId', i) as string; - - const itemId = this.getNodeParameter('itemId', i) as string; - - const properties = this.getNodeParameter( - 'fieldsUi.fieldValues', - i, - [], - ) as IDataObject[]; - - const live = this.getNodeParameter('live', i) as boolean; - - const fields = {} as IDataObject; - - properties.forEach((data) => (fields[data.fieldId as string] = data.fieldValue)); - - const body: IDataObject = { - fields, - }; - - responseData = await webflowApiRequest.call( - this, - 'PUT', - `/collections/${collectionId}/items/${itemId}`, - body, - { live }, - ); - } - } - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } catch (error) { - if (this.continueOnFail(error)) { - returnData.push({ json: { error: error.message } }); - continue; - } - throw error; - } - } - - return [returnData]; + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts index 632bfb9534..cdd2ee3814 100644 --- a/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts +++ b/packages/nodes-base/nodes/Webflow/WebflowTrigger.node.ts @@ -1,273 +1,24 @@ -import type { - IHookFunctions, - IWebhookFunctions, - IDataObject, - ILoadOptionsFunctions, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - IWebhookResponseData, -} from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; +import { WebflowTriggerV1 } from './V1/WebflowTriggerV1.node'; +import { WebflowTriggerV2 } from './V2/WebflowTriggerV2.node'; -import { webflowApiRequest } from './GenericFunctions'; - -export class WebflowTrigger implements INodeType { - description: INodeTypeDescription = { - displayName: 'Webflow Trigger', - name: 'webflowTrigger', - icon: 'file:webflow.svg', - group: ['trigger'], - version: 1, - description: 'Handle Webflow events via webhooks', - defaults: { - name: 'Webflow Trigger', - }, - inputs: [], - outputs: ['main'], - credentials: [ - { - name: 'webflowApi', - required: true, - displayOptions: { - show: { - authentication: ['accessToken'], - }, - }, - }, - { - name: 'webflowOAuth2Api', - required: true, - displayOptions: { - show: { - authentication: ['oAuth2'], - }, - }, - }, - ], - webhooks: [ - { - name: 'default', - httpMethod: 'POST', - responseMode: 'onReceived', - path: 'webhook', - }, - ], - properties: [ - { - displayName: 'Authentication', - name: 'authentication', - type: 'options', - options: [ - { - name: 'Access Token', - value: 'accessToken', - }, - { - name: 'OAuth2', - value: 'oAuth2', - }, - ], - default: 'accessToken', - }, - { - displayName: 'Site Name or ID', - name: 'site', - type: 'options', - required: true, - default: '', - typeOptions: { - loadOptionsMethod: 'getSites', - }, - description: - 'Site that will trigger the events. Choose from the list, or specify an ID using an expression.', - }, - { - displayName: 'Event', - name: 'event', - type: 'options', - required: true, - options: [ - { - name: 'Collection Item Created', - value: 'collection_item_created', - }, - { - name: 'Collection Item Deleted', - value: 'collection_item_deleted', - }, - { - name: 'Collection Item Updated', - value: 'collection_item_changed', - }, - { - name: 'Ecomm Inventory Changed', - value: 'ecomm_inventory_changed', - }, - { - name: 'Ecomm New Order', - value: 'ecomm_new_order', - }, - { - name: 'Ecomm Order Changed', - value: 'ecomm_order_changed', - }, - { - name: 'Form Submission', - value: 'form_submission', - }, - { - name: 'Site Publish', - value: 'site_publish', - }, - ], - default: 'form_submission', - }, - // { - // displayName: 'All collections', - // name: 'allCollections', - // type: 'boolean', - // displayOptions: { - // show: { - // event: [ - // 'collection_item_created', - // 'collection_item_changed', - // 'collection_item_deleted', - // ], - // }, - // }, - // required: false, - // default: true, - // description: 'Receive events from all collections', - // }, - // { - // displayName: 'Collection', - // name: 'collection', - // type: 'options', - // required: false, - // default: '', - // typeOptions: { - // loadOptionsMethod: 'getCollections', - // loadOptionsDependsOn: [ - // 'site', - // ], - // }, - // description: 'Collection that will trigger the events', - // displayOptions: { - // show: { - // allCollections: [ - // false, - // ], - // }, - // }, - // }, - ], - }; - - methods = { - loadOptions: { - // Get all the sites to display them to user so that they can - // select them easily - async getSites(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - const sites = await webflowApiRequest.call(this, 'GET', '/sites'); - for (const site of sites) { - const siteName = site.name; - const siteId = site._id; - returnData.push({ - name: siteName, - value: siteId, - }); - } - return returnData; - }, - // async getCollections(this: ILoadOptionsFunctions): Promise { - // const returnData: INodePropertyOptions[] = []; - // const siteId = this.getCurrentNodeParameter('site'); - // const collections = await webflowApiRequest.call(this, 'GET', `/sites/${siteId}/collections`); - // for (const collection of collections) { - // returnData.push({ - // name: collection.name, - // value: collection._id, - // }); - // } - // return returnData; - // }, - }, - }; - - webhookMethods = { - default: { - async checkExists(this: IHookFunctions): Promise { - const webhookData = this.getWorkflowStaticData('node'); - const webhookUrl = this.getNodeWebhookUrl('default'); - const siteId = this.getNodeParameter('site') as string; - - const event = this.getNodeParameter('event') as string; - const registeredWebhooks = (await webflowApiRequest.call( - this, - 'GET', - `/sites/${siteId}/webhooks`, - )) as IDataObject[]; - - for (const webhook of registeredWebhooks) { - if (webhook.url === webhookUrl && webhook.triggerType === event) { - webhookData.webhookId = webhook._id; - return true; - } - } - - return false; - }, - - async create(this: IHookFunctions): Promise { - const webhookUrl = this.getNodeWebhookUrl('default'); - const webhookData = this.getWorkflowStaticData('node'); - const siteId = this.getNodeParameter('site') as string; - const event = this.getNodeParameter('event') as string; - const endpoint = `/sites/${siteId}/webhooks`; - const body: IDataObject = { - site_id: siteId, - triggerType: event, - url: webhookUrl, - }; - - // if (event.startsWith('collection')) { - // const allCollections = this.getNodeParameter('allCollections') as boolean; - - // if (allCollections === false) { - // body.filter = { - // 'cid': this.getNodeParameter('collection') as string, - // }; - // } - // } - - const { _id } = await webflowApiRequest.call(this, 'POST', endpoint, body); - webhookData.webhookId = _id; - return true; - }, - async delete(this: IHookFunctions): Promise { - let responseData; - const webhookData = this.getWorkflowStaticData('node'); - const siteId = this.getNodeParameter('site') as string; - const endpoint = `/sites/${siteId}/webhooks/${webhookData.webhookId}`; - try { - responseData = await webflowApiRequest.call(this, 'DELETE', endpoint); - } catch (error) { - return false; - } - if (!responseData.deleted) { - return false; - } - delete webhookData.webhookId; - return true; - }, - }, - }; - - async webhook(this: IWebhookFunctions): Promise { - const req = this.getRequestObject(); - return { - workflowData: [this.helpers.returnJsonArray(req.body as IDataObject[])], +export class WebflowTrigger extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Webflow Trigger', + name: 'webflowTrigger', + icon: 'file:webflow.svg', + group: ['trigger'], + description: 'Handle Webflow events via webhooks', + defaultVersion: 2, }; + + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new WebflowTriggerV1(baseDescription), + 2: new WebflowTriggerV2(baseDescription), + }; + + super(nodeVersions, baseDescription); } }