From c420c053ad40f2b2e126fb0c3dd58e8a19f5b3f5 Mon Sep 17 00:00:00 2001 From: Valentina Lilova Date: Tue, 17 Sep 2024 12:57:34 +0200 Subject: [PATCH] Add a trigger node --- .../GoogleMyBusinessTrigger.node.json | 18 ++ .../GoogleMyBusinessTrigger.node.ts | 274 ++++++++++++++++++ packages/nodes-base/package.json | 1 + 3 files changed, 293 insertions(+) create mode 100644 packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.json create mode 100644 packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.ts diff --git a/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.json b/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.json new file mode 100644 index 0000000000..a456fdd38c --- /dev/null +++ b/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.googleMyBusinessTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Communication"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.googlemybusinesstrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.ts b/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.ts new file mode 100644 index 0000000000..a105e9413c --- /dev/null +++ b/packages/nodes-base/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.ts @@ -0,0 +1,274 @@ +import { + NodeApiError, + NodeConnectionType, + type IPollFunctions, + type IDataObject, + type ILoadOptionsFunctions, + type INodeExecutionData, + type INodeType, + type INodeTypeDescription, + type INodeListSearchItems, + type INodeListSearchResult, + NodeOperationError, +} from 'n8n-workflow'; +import { googleApiRequest } from './GenericFunctions'; + +export class GoogleMyBusinessTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google My Business Trigger', + name: 'googleMyBusinessTrigger', + icon: 'file:googleMyBusines.svg', + group: ['trigger'], + version: 1, + description: + 'Fetches reviews from Google My Business and starts the workflow on specified polling intervals.', + subtitle: '={{"Google My Business Trigger"}}', + defaults: { + name: 'Google My Business Trigger', + }, + credentials: [ + { + name: 'googleMyBusinessOAuth2Api', + required: true, + }, + ], + polling: true, + inputs: [], + outputs: [NodeConnectionType.Main], + properties: [ + { + displayName: 'Event', + name: 'event', + required: true, + type: 'options', + noDataExpression: true, + default: 'reviewAdded', + options: [ + { + name: 'Review Added', + value: 'reviewAdded', + }, + ], + }, + { + displayName: 'Account', + name: 'account', + required: true, + type: 'resourceLocator', + default: '', + description: 'The Google My Business account name', + displayOptions: { show: { event: ['reviewAdded'] } }, + modes: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + hint: 'Enter the account name', + validation: [ + { + type: 'regex', + properties: { + regex: 'accounts/[0-9]+', + errorMessage: 'The name must start with "accounts/"', + }, + }, + ], + placeholder: 'accounts/012345678901234567890', + }, + { + displayName: 'List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchAccounts', + searchable: true, + }, + }, + ], + }, + { + displayName: 'Location', + name: 'location', + required: true, + type: 'resourceLocator', + default: '', + description: 'The specific location or business associated with the account', + displayOptions: { show: { event: ['reviewAdded'] } }, + modes: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + hint: 'Enter the location name', + validation: [ + { + type: 'regex', + properties: { + regex: 'locations/[0-9]+', + errorMessage: 'The name must start with "locations/"', + }, + }, + ], + placeholder: 'locations/012345678901234567', + }, + { + displayName: 'List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'searchLocations', + searchable: true, + }, + }, + ], + }, + ], + }; + + methods = { + listSearch: { + // Docs can be found here: + // https://developers.google.com/my-business/reference/accountmanagement/rest/v1/accounts/list + async searchAccounts( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, + ): Promise { + const query: IDataObject = {}; + if (paginationToken) { + query.pageToken = paginationToken; + } + + const responseData: IDataObject = await googleApiRequest.call( + this, + 'GET', + '', + {}, + { + pageSize: 20, + ...query, + }, + 'https://mybusinessaccountmanagement.googleapis.com/v1/accounts', + ); + + const accounts = responseData.accounts as Array<{ name: string }>; + + const results: INodeListSearchItems[] = accounts + .map((a) => ({ + name: a.name, + value: a.name, + })) + .filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; + }); + + return { results, paginationToken: responseData.nextPageToken }; + }, + // Docs can be found here: + // https://developers.google.com/my-business/reference/businessinformation/rest/v1/accounts.locations/list + async searchLocations( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, + ): Promise { + const query: IDataObject = {}; + if (paginationToken) { + query.pageToken = paginationToken; + } + + const account = (this.getNodeParameter('account') as IDataObject).value as string; + + const responseData: IDataObject = await googleApiRequest.call( + this, + 'GET', + '', + {}, + { + readMask: 'name', + pageSize: 100, + ...query, + }, + `https://mybusinessbusinessinformation.googleapis.com/v1/${account}/locations`, + ); + + const locations = responseData.locations as Array<{ name: string }>; + + const results: INodeListSearchItems[] = locations + .map((a) => ({ + name: a.name, + value: a.name, + })) + .filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; + }); + + return { results, paginationToken: responseData.nextPageToken }; + }, + }, + }; + + async poll(this: IPollFunctions): Promise { + if (this.getMode() === 'manual') { + throw new NodeOperationError( + this.getNode(), + 'This trigger node is meant to be used only for pooling and does not support manual executions.', + ); + } + + const nodeStaticData = this.getWorkflowStaticData('node'); + let responseData; + + // const event = this.getNodeParameter('event') as string; // Currently there is only one event + const account = (this.getNodeParameter('account') as { value: string; mode: string }).value; + const location = (this.getNodeParameter('location') as { value: string; mode: string }).value; + + try { + responseData = (await googleApiRequest.call( + this, + 'GET', + `/${account}/${location}/reviews`, + {}, + { + pageSize: 50, // Maximal page size for this endpoint + }, + )) as { reviews: IDataObject[]; totalReviewCount: number; nextPageToken?: string }; + + // During the first execution there is no delta + if (!nodeStaticData.totalReviewCountLastTimeChecked) { + nodeStaticData.totalReviewCountLastTimeChecked = responseData.totalReviewCount; + return null; + } + + // When count did't change the node shouldn't trigger + if ( + !responseData?.reviews?.length || + nodeStaticData?.totalReviewCountLastTimeChecked === responseData?.totalReviewCount + ) { + return null; + } + + const numNewReviews = + // @ts-ignore + responseData.totalReviewCount - nodeStaticData.totalReviewCountLastTimeChecked; + nodeStaticData.totalReviewCountLastTimeChecked = responseData.totalReviewCount; + + // By default the reviews will be sorted by updateTime in descending order + // Return only the delta reviews since last pooling + responseData = responseData.reviews.slice(0, numNewReviews); + + if (Array.isArray(responseData) && responseData.length) { + return [this.helpers.returnJsonArray(responseData)]; + } + + return null; + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f62a26da9d..c5de50d91c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -530,6 +530,7 @@ "dist/nodes/Google/Gmail/GmailTrigger.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", "dist/nodes/Google/MyBusiness/GoogleMyBusiness.node.js", + "dist/nodes/Google/MyBusiness/GoogleMyBusinessTrigger.node.js", "dist/nodes/Google/Perspective/GooglePerspective.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", "dist/nodes/Google/Sheet/GoogleSheetsTrigger.node.js",