diff --git a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts index 616117fd86..c008e35a33 100644 --- a/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts +++ b/packages/nodes-base/nodes/Google/Calendar/EventDescription.ts @@ -700,14 +700,14 @@ export const eventFields: INodeProperties[] = [ 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: 'End Time', + displayName: 'Start Time', name: 'timeMax', type: 'dateTime', default: '', description: `Upper bound (exclusive) for an event's start time to filter by`, }, { - displayName: 'Start Time', + displayName: 'End Time', name: 'timeMin', type: 'dateTime', default: '', diff --git a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts index 4b9eff2928..6ffd7404f5 100644 --- a/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Calendar/GenericFunctions.ts @@ -9,10 +9,12 @@ import { } from 'n8n-core'; import { - IDataObject, NodeApiError, + IDataObject, + IPollFunctions, + NodeApiError, } from 'n8n-workflow'; -export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any const options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', @@ -37,7 +39,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF } } -export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts new file mode 100644 index 0000000000..1972972dcb --- /dev/null +++ b/packages/nodes-base/nodes/Google/Calendar/GoogleCalendarTrigger.node.ts @@ -0,0 +1,215 @@ + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IPollFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import * as moment from 'moment'; + +export class GoogleCalendarTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Calendar Trigger', + name: 'googleCalendarTrigger', + icon: 'file:googleCalendar.svg', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["triggerOn"]}}', + description: 'Starts the workflow when Google Calendar events occur', + defaults: { + name: 'Google Calendar Trigger', + color: '#3E87E4', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'googleCalendarOAuth2Api', + required: true, + }, + ], + polling: true, + properties: [ + { + displayName: 'Calendar Name/ID', + name: 'calendarId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + default: '', + }, + { + displayName: 'Trigger On', + name: 'triggerOn', + type: 'options', + required: true, + default: '', + options: [ + { + name: 'Event Created', + value: 'eventCreated', + }, + { + name: 'Event Ended', + value: 'eventEnded', + }, + { + name: 'Event Started', + value: 'eventStarted', + }, + { + name: 'Event Updated', + value: 'eventUpdated', + }, + ], + description: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Match Term', + name: 'matchTerm', + type: 'string', + default: '', + description: 'Free text search terms to filter events that match these terms in any field, except for extended properties', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getCalendars( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const calendars = await googleApiRequestAllItems.call( + this, + 'items', + 'GET', + '/calendar/v3/users/me/calendarList', + ); + for (const calendar of calendars) { + returnData.push({ + name: calendar.summary, + value: calendar.id, + }); + } + return returnData; + }, + }, + }; + + async poll(this: IPollFunctions): Promise { + const poolTimes = this.getNodeParameter('pollTimes.item', []) as IDataObject[]; + const triggerOn = this.getNodeParameter('triggerOn', '') as string; + const calendarId = this.getNodeParameter('calendarId') as string; + const webhookData = this.getWorkflowStaticData('node'); + const matchTerm = this.getNodeParameter('options.matchTerm', '') as string; + + if (poolTimes.length === 0) { + throw new NodeOperationError( + this.getNode(), + 'Please set a poll time', + ); + } + + if (triggerOn === '') { + throw new NodeOperationError( + this.getNode(), + 'Please select an event', + ); + } + + if (calendarId === '') { + throw new NodeOperationError( + this.getNode(), + 'Please select a calendar', + ); + } + + const now = moment().utc().format(); + + const startDate = webhookData.lastTimeChecked as string || now; + + const endDate = now; + + const qs: IDataObject = { + showDeleted: false, + }; + + if (matchTerm !== '') { + qs.q = matchTerm; + } + + let events; + + if (triggerOn === 'eventCreated' || triggerOn === 'eventUpdated') { + Object.assign(qs, { + updatedMin: startDate, + orderBy: 'updated', + }); + } else if (triggerOn === 'eventStarted' || triggerOn === 'eventEnded') { + Object.assign(qs, { + singleEvents: true, + timeMin: moment(startDate).startOf('second').utc().format(), + timeMax: moment(endDate).endOf('second').utc().format(), + orderBy: 'startTime', + }); + } + + if (this.getMode() === 'manual') { + delete qs.updatedMin; + delete qs.timeMin; + delete qs.timeMax; + + qs.maxResults = 1; + events = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + events = events.items; + } else { + events = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + if (triggerOn === 'eventCreated') { + events = events.filter((event: { created: string }) => moment(event.created).isBetween(startDate, endDate)); + } else if (triggerOn === 'eventUpdated') { + events = events.filter((event: { created: string, updated: string }) => !moment(moment(event.created).format('YYYY-MM-DDTHH:mm:ss')).isSame(moment(event.updated).format('YYYY-MM-DDTHH:mm:ss'))); + } else if (triggerOn === 'eventStarted') { + events = events.filter((event: { start: { dateTime: string } }) => moment(event.start.dateTime).isBetween(startDate, endDate, null, '[]')); + } else if (triggerOn === 'eventEnded') { + events = events.filter((event: { end: { dateTime: string } }) => moment(event.end.dateTime).isBetween(startDate, endDate, null, '[]')); + } + } + + webhookData.lastTimeChecked = endDate; + + if (Array.isArray(events) && events.length) { + return [this.helpers.returnJsonArray(events)]; + } + + if (this.getMode() === 'manual') { + throw new NodeApiError(this.getNode(), { message: 'No data with the current filter could be found' }); + } + + return null; + } +} \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5848875ac4..30148d7a8d 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -421,6 +421,7 @@ "dist/nodes/Google/BigQuery/GoogleBigQuery.node.js", "dist/nodes/Google/Books/GoogleBooks.node.js", "dist/nodes/Google/Calendar/GoogleCalendar.node.js", + "dist/nodes/Google/Calendar/GoogleCalendarTrigger.node.js", "dist/nodes/Google/CloudNaturalLanguage/GoogleCloudNaturalLanguage.node.js", "dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Docs/GoogleDocs.node.js", diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index db3c8a3248..a5782e2e46 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -57,7 +57,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] multipleValueButtonText: 'Add Poll Time', }, default: {}, - description: 'Time at which polling should occur.', + description: 'Time at which polling should occur', placeholder: 'Add Poll Time', options: [ { @@ -115,7 +115,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] }, }, default: 14, - description: 'The hour of the day to trigger (24h format).', + description: 'The hour of the day to trigger (24h format)', }, { displayName: 'Minute', @@ -131,7 +131,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] }, }, default: 0, - description: 'The minute of the day to trigger.', + description: 'The minute of the day to trigger', }, { displayName: 'Day of Month', @@ -147,7 +147,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] maxValue: 31, }, default: 1, - description: 'The day of the month to trigger.', + description: 'The day of the month to trigger', }, { displayName: 'Weekday', @@ -189,7 +189,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] }, ], default: '1', - description: 'The weekday to trigger.', + description: 'The weekday to trigger', }, { displayName: 'Cron Expression', @@ -218,7 +218,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] }, }, default: 2, - description: 'All how many X minutes/hours it should trigger.', + description: 'All how many X minutes/hours it should trigger', }, { displayName: 'Unit', @@ -240,7 +240,7 @@ export function getSpecialNodeParameters(nodeType: INodeType): INodeProperties[] }, ], default: 'hours', - description: 'If it should trigger all X minutes or hours.', + description: 'If it should trigger all X minutes or hours', }, ], },