From fb4ae619aafa22f35ee207eefb354f9d089fd371 Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Thu, 12 Sep 2024 07:45:53 +0300 Subject: [PATCH] get event instances --- .../nodes/Google/Calendar/EventDescription.ts | 40 ++++++++--- .../nodes/Google/Calendar/GenericFunctions.ts | 61 +++++++++++++++- .../Google/Calendar/GoogleCalendar.node.ts | 72 +++++++++++++++++-- 3 files changed, 157 insertions(+), 16 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts index 57b5062c6c..a45e958a10 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts @@ -553,6 +553,18 @@ export const eventFields: INodeProperties[] = [ description: 'The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned.', }, + { + displayName: 'Return Next Instance of Recurring Event', + name: 'returnNextInstance', + type: 'boolean', + default: true, + description: 'Whether to return next instances of recurring event, instead of event itself', + displayOptions: { + show: { + '@version': [1.2], + }, + }, + }, { displayName: 'Timezone', name: 'timeZone', @@ -656,6 +668,14 @@ export const eventFields: INodeProperties[] = [ default: '', description: 'At least some part of the event must be before this time', }, + { + displayName: 'Expand Events', + name: 'singleEvents', + type: 'boolean', + default: false, + description: + 'Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves', + }, { displayName: 'Fields', name: 'fields', @@ -708,6 +728,18 @@ export const eventFields: INodeProperties[] = [ description: 'Free text search terms to find events that match these terms in any field, except for extended properties', }, + { + displayName: 'Return Next Instance of Recurring Event', + name: 'returnNextInstance', + type: 'boolean', + default: true, + description: 'Whether to return next instances of recurring event, instead of event itself', + displayOptions: { + show: { + '@version': [1.2], + }, + }, + }, { displayName: 'Show Deleted', name: 'showDeleted', @@ -723,14 +755,6 @@ export const eventFields: INodeProperties[] = [ default: false, description: 'Whether to include hidden invitations in the result', }, - { - displayName: 'Single Events', - name: 'singleEvents', - type: 'boolean', - default: false, - description: - 'Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves', - }, { displayName: 'Timezone', name: 'timeZone', diff --git a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts index e0fc029de5..1da3e2cd9e 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts @@ -3,13 +3,14 @@ import type { IExecuteFunctions, IHttpRequestMethods, ILoadOptionsFunctions, + INode, INodeListSearchItems, INodeListSearchResult, IPollFunctions, IRequestOptions, JsonObject, } from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; +import { NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import moment from 'moment-timezone'; import { RRule } from 'rrule'; @@ -52,7 +53,6 @@ export async function googleApiRequestAllItems( propertyName: string, method: IHttpRequestMethods, endpoint: string, - body: any = {}, query: IDataObject = {}, ): Promise { @@ -212,3 +212,60 @@ export function addTimezoneToDate(date: string, timezone: string) { if (hasTimezone(date)) return date; return moment.tz(date, timezone).utc().format(); } + +async function requestWithRetries( + node: INode, + requestFn: () => Promise, + retryCount: number = 0, + maxRetries: number = 10, + itemIndex: number = 0, +): Promise { + try { + return await requestFn(); + } catch (error) { + if (!(error instanceof NodeApiError)) { + throw new NodeOperationError(node, error.message, { itemIndex }); + } + if (retryCount >= maxRetries) throw error; + + if (error.httpCode === '403' || error.httpCode === '429') { + const delay = 1000 * Math.pow(2, retryCount); + + console.log(`Rate limit hit. Retrying in ${delay}ms... (Attempt ${retryCount + 1})`); + + await sleep(delay); + return await requestWithRetries(node, requestFn, retryCount + 1, maxRetries, itemIndex); + } else { + throw error; + } + } +} + +export async function googleApiRequestWithRetries({ + context, + method, + resource, + body = {}, + qs = {}, + uri, + headers = {}, + itemIndex = 0, +}: { + context: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions; + method: IHttpRequestMethods; + resource: string; + body?: any; + qs?: IDataObject; + uri?: string; + headers?: IDataObject; + itemIndex?: number; +}) { + const requestFn = async (): Promise => { + return await googleApiRequest.call(context, method, resource, body, qs, uri, headers); + }; + + const retryCount = 0; + const maxRetries = 10; + + return await requestWithRetries(context.getNode(), requestFn, retryCount, maxRetries, itemIndex); +} diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts index dfdc9698b7..ada16d2156 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendar.node.ts @@ -20,6 +20,8 @@ import { getTimezones, googleApiRequest, googleApiRequestAllItems, + googleApiRequestWithRetries, + type RecurentEvent, } from './GenericFunctions'; import { eventFields, eventOperations } from './EventDescription'; @@ -34,7 +36,7 @@ export class GoogleCalendar implements INodeType { name: 'googleCalendar', icon: 'file:googleCalendar.svg', group: ['input'], - version: [1, 1.1], + version: [1, 1.1, 1.2], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Google Calendar API', defaults: { @@ -48,6 +50,16 @@ export class GoogleCalendar implements INodeType { required: true, }, ], + hints: [ + { + message: + "Turn off 'Return Next Instance of Recurring Event' option to improve efficiency and return reccuring events itself instead of their next event instance", + displayCondition: + '={{ $nodeVersion >= 1.2 && $parameter["operation"] === "getAll" && $parameter["options"]["returnNextInstance"] !== false}}', + whenToDisplay: 'beforeExecution', + location: 'outputPane', + }, + ], properties: [ { displayName: 'Resource', @@ -381,16 +393,33 @@ export class GoogleCalendar implements INodeType { if (tz) { qs.timeZone = tz; } - responseData = await googleApiRequest.call( + const event = (await googleApiRequest.call( this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs, - ); + )) as IDataObject; - if (responseData) { - responseData = addNextOccurrence([responseData]); + if (event) { + if (nodeVersion >= 1.2 && options.returnNextInstance !== false && event.recurrence) { + const eventInstances = + (( + (await googleApiRequest.call( + this, + 'GET', + `/calendar/v3/calendars/${calendarId}/events/${event.id}/instances`, + {}, + { + timeMin: new Date().toISOString(), + maxResults: 1, + }, + )) as IDataObject + ).items as IDataObject[]) || []; + responseData = eventInstances[0] ? [eventInstances[0]] : [responseData]; + } else { + responseData = addNextOccurrence([responseData as RecurentEvent]); + } } } //https://developers.google.com/calendar/v3/reference/events/list @@ -462,7 +491,38 @@ export class GoogleCalendar implements INodeType { } if (responseData) { - responseData = addNextOccurrence(responseData); + if ( + nodeVersion >= 1.2 && + !options.singleEvents && + options.returnNextInstance !== false + ) { + const updatedEvents: IDataObject[] = []; + + for (const event of responseData) { + if (event.recurrence) { + const eventInstances = + (( + (await googleApiRequestWithRetries({ + context: this, + method: 'GET', + resource: `/calendar/v3/calendars/${calendarId}/events/${event.id}/instances`, + qs: { + timeMin: new Date().toISOString(), + maxResults: 1, + }, + itemIndex: i, + })) as IDataObject + ).items as IDataObject[]) || []; + updatedEvents.push(eventInstances[0] || event); + continue; + } + + updatedEvents.push(event); + } + responseData = updatedEvents; + } else { + responseData = addNextOccurrence(responseData); + } } } //https://developers.google.com/calendar/v3/reference/events/patch