diff --git a/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts new file mode 100644 index 0000000000..facd5e8fac --- /dev/null +++ b/packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts @@ -0,0 +1,49 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/calendar.events', +]; + +export class GoogleOAuth2Api implements ICredentialType { + name = 'googleOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Google OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.google.com/o/oauth2/v2/auth', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://oauth2.googleapis.com/token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'access_type=offline', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/EventDescription.ts b/packages/nodes-base/nodes/Google/EventDescription.ts new file mode 100644 index 0000000000..e92582a847 --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventDescription.ts @@ -0,0 +1,1106 @@ +import { INodeProperties } from "n8n-workflow"; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add a event to calendar', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an event', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all events from a calendar', + }, + { + name: 'Update', + value: 'update', + description: 'Update an event', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + +/* -------------------------------------------------------------------------- */ +/* event:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + 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: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'create', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +/* -------------------------------------------------------------------------- */ +/* event:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Options', + default: {}, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + 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: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'iCalUID', + name: 'iCalUID', + type: 'string', + default: '', + description: 'Specifies event ID in the iCalendar format to be included in the response', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + 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: 'Order By', + name: 'orderBy', + type: 'options', + options: [ + { + name: 'Start Time', + value: 'startTime', + description: 'Order by the start date/time (ascending). This is only available when querying single events (i.e. the parameter singleEvents is True)', + }, + { + name: 'Updated', + value: 'updated', + description: 'Order by last modification time (ascending).', + }, + ], + default: '', + description: 'The order of the events returned in the result.', + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: 'Free text search terms to find events that match these terms in any field, except for extended properties.', + }, + { + displayName: 'Show Deleted', + name: 'showDeleted', + type: 'boolean', + default: false, + description: 'Whether to include deleted events (with status equals "cancelled") in the result.', + }, + { + displayName: 'Show Hidden Invitations', + name: 'showHiddenInvitations', + type: 'boolean', + 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: 'Time Max', + name: 'timeMax', + type: 'dateTime', + default: '', + description: `Upper bound (exclusive) for an event's start time to filter by`, + }, + { + displayName: 'Time Min', + name: 'timeMin', + type: 'dateTime', + default: '', + description: `Lower bound (exclusive) for an event's end time to filter by`, + }, + { + displayName: 'Timezone', + name: 'timeZone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: `Time zone used in the response. The default is the time zone of the calendar.`, + }, + { + displayName: 'Updated Min', + name: 'updatedMin', + type: 'dateTime', + default: '', + description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. + When specified, entries deleted since this time will always be included regardless of showDeleted`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* event:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Calendar', + name: 'calendar', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCalendars', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: '', + }, + { + displayName: 'Use Default Reminders', + name: 'useDefaultReminders', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + default: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'event', + ], + }, + }, + options: [ + { + displayName: 'All Day', + name: 'allday', + type: 'boolean', + options: [ + { + name: 'Yes', + value: 'yes', + }, + { + name: 'No', + value: 'no', + }, + ], + default: 'no', + description: 'Wheater the event is all day or not', + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Attendee', + }, + default: '', + description: 'The attendees of the event', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getColors', + }, + default: '', + description: 'The color of the event.', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + description: 'End time of the event.', + }, + { + displayName: 'Guests Can Invite Others', + name: 'guestsCanInviteOthers', + type: 'boolean', + default: true, + description: 'Whether attendees other than the organizer can invite others to the event', + }, + { + displayName: 'Guests Can Modify', + name: 'guestsCanModify', + type: 'boolean', + default: false, + description: 'Whether attendees other than the organizer can modify the event', + }, + { + displayName: 'Guests Can See Other Guests', + name: 'guestsCanSeeOtherGuests', + type: 'boolean', + default: true, + description: `Whether attendees other than the organizer can see who the event's attendees are.`, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + description: 'Opaque identifier of the event', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Geographic location of the event as free-form text.', + }, + { + displayName: 'Max Attendees', + name: 'maxAttendees', + type: 'number', + default: 0, + 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: 'Repeat Frecuency', + name: 'repeatFrecuency', + type: 'options', + options: [ + { + name: 'Daily', + value: 'Daily', + }, + { + name: 'Weekly', + value: 'weekly', + }, + { + name: 'Monthly', + value: 'monthly', + }, + { + name: 'Yearly', + value: 'yearly', + }, + ], + default: '', + }, + { + displayName: 'Repeat Until', + name: 'repeatUntil', + type: 'dateTime', + default: '', + }, + { + displayName: 'Repeat How Many Times?', + name: 'repeatHowManyTimes', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start time of the event.', + }, + { + displayName: 'Send Updates', + name: 'sendUpdates', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: ' Notifications are sent to all guests', + }, + { + name: 'External Only', + value: 'externalOnly', + description: 'Notifications are sent to non-Google Calendar guests only', + }, + { + name: 'None', + value: 'none', + description: ' No notifications are sent. This value should only be used for migration use case', + }, + ], + description: 'Whether to send notifications about the creation of the new event', + default: '', + }, + { + displayName: 'Summary', + name: 'summary', + type: 'string', + default: '', + description: 'Title of the event.', + }, + { + displayName: 'Show Me As', + name: 'showMeAs', + type: 'options', + options: [ + { + name: 'Available', + value: 'transparent', + description: 'The event does not block time on the calendar', + }, + { + name: 'Busy', + value: 'opaque', + description: ' The event does block time on the calendar.', + }, + ], + default: 'opaque', + description: 'Whether the event blocks time on the calendar', + }, + { + displayName: 'Timezone', + name: 'timezone', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + default: '', + description: 'The timezone the event will have set. By default events are schedule on n8n timezone ' + }, + { + displayName: 'Visibility', + name: 'visibility', + type: 'options', + options: [ + { + name: 'Confidential', + value: 'confidential', + description: 'The event is private. This value is provided for compatibility reasons.', + }, + { + name: 'Default', + value: 'default', + description: ' Uses the default visibility for events on the calendar.', + }, + { + name: 'Public', + value: 'public', + description: 'The event is public and event details are visible to all readers of the calendar.', + }, + { + name: 'Private', + value: 'private', + description: 'The event is private and only event attendees may view event details.', + }, + ], + default: 'default', + description: 'Visibility of the event.', + }, + ], + }, + { + displayName: 'Reminders', + name: 'remindersUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Reminder', + typeOptions: { + multipleValues: true, + }, + required: false, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'update', + ], + useDefaultReminders: [ + false, + ], + }, + }, + options: [ + { + name: 'remindersValues', + displayName: 'Reminder', + values: [ + { + displayName: 'Method', + name: 'method', + type: 'options', + options: [ + { + name: 'Email', + value: 'email', + }, + { + name: 'Popup', + value: 'popup', + }, + ], + default: '', + }, + { + displayName: 'Minutes Before', + name: 'minutes', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 40320, + }, + default: 0, + }, + ], + } + ], + description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/EventInterface.ts b/packages/nodes-base/nodes/Google/EventInterface.ts new file mode 100644 index 0000000000..72bf96cc80 --- /dev/null +++ b/packages/nodes-base/nodes/Google/EventInterface.ts @@ -0,0 +1,26 @@ +import { IDataObject } from "n8n-workflow"; + +export interface IReminder { + useDefault?: boolean; + overrides?: IDataObject[]; +} + +export interface IEvent { + attendees?: IDataObject[]; + colorId?: string; + description?: string; + end?: IDataObject; + guestsCanInviteOthers?: boolean; + guestsCanModify?: boolean; + guestsCanSeeOtherGuests?: boolean; + id?: string; + location?: string; + maxAttendees?: number; + recurrence?: string[]; + reminders?: IReminder; + sendUpdates?: string; + start?: IDataObject; + summary?: string; + transparency?: string; + visibility?: string; +} diff --git a/packages/nodes-base/nodes/Google/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GenericFunctions.ts new file mode 100644 index 0000000000..80edb44370 --- /dev/null +++ b/packages/nodes-base/nodes/Google/GenericFunctions.ts @@ -0,0 +1,57 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { + IDataObject +} 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 + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://www.googleapis.com${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.requestOAuth.call(this, 'googleOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.maxResults = 100; + + do { + responseData = await googleApiRequest.call(this, method, endpoint, body, query); + query.pageToken = responseData['nextPageToken']; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData['nextPageToken'] !== undefined && + responseData['nextPageToken'] !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts new file mode 100644 index 0000000000..c7eb35794e --- /dev/null +++ b/packages/nodes-base/nodes/Google/GoogleCalendar.node.ts @@ -0,0 +1,412 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + eventOperations, + eventFields, +} from './EventDescription'; + +import { + IEvent, +} from './EventInterface'; + +import * as moment from 'moment-timezone'; + +export class GoogleCalendar implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Calendar', + name: 'googleCalendar', + icon: 'file:googleCalendar.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Calendar API.', + defaults: { + name: 'Google Calendar', + color: '#3E87E4', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + ], + default: 'event', + description: 'The resource to operate on.', + }, + ...eventOperations, + ...eventFields, + ], + }; + + 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) { + const calendarName = calendar.summary; + const calendarId = calendar.id; + returnData.push({ + name: calendarName, + value: calendarId, + }); + } + return returnData; + }, + // Get all the colors to display them to user so that he can + // select them easily + async getColors(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors'); + for (const key of Object.keys(calendar)) { + const colorName = calendar[key].background; + const colorId = key; + returnData.push({ + name: `${colorName} - ${colorId}`, + value: colorId, + }); + } + return returnData; + }, + // Get all the timezones to display them to user so that he can + // select them easily + async getTimezones(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const timezone of moment.tz.names()) { + const timezoneName = timezone; + const timezoneId = timezone; + returnData.push({ + name: timezoneName, + value: timezoneId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'event') { + //https://developers.google.com/calendar/v3/reference/events/insert + if (operation === 'create') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const start = this.getNodeParameter('start', i) as string; + const end = this.getNodeParameter('end', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.maxAttendees) { + qs.maxAttendees = additionalFields.maxAttendees as number; + } + if (additionalFields.sendNotifications) { + qs.sendNotifications = additionalFields.sendNotifications as boolean; + } + if (additionalFields.sendUpdates) { + qs.sendUpdates = additionalFields.sendUpdates as string; + } + const body: IEvent = { + start: { + dateTime: start, + timeZone: additionalFields.timeZone || this.getTimezone(), + }, + end: { + dateTime: end, + timeZone: additionalFields.timeZone || this.getTimezone(), + } + }; + if (additionalFields.attendees) { + body.attendees = (additionalFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (additionalFields.color) { + body.colorId = additionalFields.color as string; + } + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + if (additionalFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = additionalFields.guestsCanInviteOthers as boolean; + } + if (additionalFields.guestsCanModify) { + body.guestsCanModify = additionalFields.guestsCanModify as boolean; + } + if (additionalFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = additionalFields.guestsCanSeeOtherGuests as boolean; + } + if (additionalFields.id) { + body.id = additionalFields.id as string; + } + if (additionalFields.location) { + body.location = additionalFields.location as string; + } + if (additionalFields.summary) { + body.summary = additionalFields.summary as string; + } + if (additionalFields.showMeAs) { + body.transparency = additionalFields.showMeAs as string; + } + if (additionalFields.visibility) { + body.visibility = additionalFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (additionalFields.allday) { + body.start = { + date: moment(start).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(end).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (additionalFields.repeatHowManyTimes + && additionalFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (additionalFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`); + } + if (additionalFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`); + } + if (additionalFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } + responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs); + } + //https://developers.google.com/calendar/v3/reference/events/delete + if (operation === 'delete') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.sendUpdates) { + qs.sendUpdates = options.sendUpdates as number; + } + responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}); + responseData = { success: true }; + } + //https://developers.google.com/calendar/v3/reference/events/get + if (operation === 'get') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs); + } + //https://developers.google.com/calendar/v3/reference/events/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const calendarId = this.getNodeParameter('calendar', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + if (options.iCalUID) { + qs.iCalUID = options.iCalUID as string; + } + if (options.maxAttendees) { + qs.maxAttendees = options.maxAttendees as number; + } + if (options.orderBy) { + qs.orderBy = options.orderBy as number; + } + if (options.query) { + qs.q = options.query as number; + } + if (options.showDeleted) { + qs.showDeleted = options.showDeleted as boolean; + } + if (options.showHiddenInvitations) { + qs.showHiddenInvitations = options.showHiddenInvitations as boolean; + } + if (options.singleEvents) { + qs.singleEvents = options.singleEvents as boolean; + } + if (options.timeMax) { + qs.timeMax = options.timeMax as string; + } + if (options.timeMin) { + qs.timeMin = options.timeMin as string; + } + if (options.timeZone) { + qs.timeZone = options.timeZone as string; + } + if (options.updatedMin) { + qs.updatedMin = options.updatedMin as string; + } + if (returnAll) { + responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + } else { + qs.maxResults = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs); + responseData = responseData.items; + } + } + //https://developers.google.com/calendar/v3/reference/events/patch + if (operation === 'update') { + const calendarId = this.getNodeParameter('calendar', i) as string; + const eventId = this.getNodeParameter('eventId', i) as string; + const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + if (updateFields.maxAttendees) { + qs.maxAttendees = updateFields.maxAttendees as number; + } + if (updateFields.sendNotifications) { + qs.sendNotifications = updateFields.sendNotifications as boolean; + } + if (updateFields.sendUpdates) { + qs.sendUpdates = updateFields.sendUpdates as string; + } + const body: IEvent = {}; + if (updateFields.start) { + body.start = { + dateTime: updateFields.start, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.end) { + body.end = { + dateTime: updateFields.end, + timeZone: updateFields.timeZone || this.getTimezone(), + }; + } + if (updateFields.attendees) { + body.attendees = (updateFields.attendees as string[]).map(attendee => { + return { email: attendee }; + }); + } + if (updateFields.color) { + body.colorId = updateFields.color as string; + } + if (updateFields.description) { + body.description = updateFields.description as string; + } + if (updateFields.guestsCanInviteOthers) { + body.guestsCanInviteOthers = updateFields.guestsCanInviteOthers as boolean; + } + if (updateFields.guestsCanModify) { + body.guestsCanModify = updateFields.guestsCanModify as boolean; + } + if (updateFields.guestsCanSeeOtherGuests) { + body.guestsCanSeeOtherGuests = updateFields.guestsCanSeeOtherGuests as boolean; + } + if (updateFields.id) { + body.id = updateFields.id as string; + } + if (updateFields.location) { + body.location = updateFields.location as string; + } + if (updateFields.summary) { + body.summary = updateFields.summary as string; + } + if (updateFields.showMeAs) { + body.transparency = updateFields.showMeAs as string; + } + if (updateFields.visibility) { + body.visibility = updateFields.visibility as string; + } + if (!useDefaultReminders) { + const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[]; + body.reminders = { + useDefault: false, + }; + if (reminders) { + body.reminders.overrides = reminders; + } + } + if (updateFields.allday + && updateFields.start + && updateFields.end) { + body.start = { + date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'), + }; + body.end = { + date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'), + }; + } + //exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z + //https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html + body.recurrence = []; + if (updateFields.repeatHowManyTimes + && updateFields.repeatUntil) { + throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`); + } + if (updateFields.repeatFrecuency) { + body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`); + } + if (updateFields.repeatHowManyTimes) { + body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`); + } + if (updateFields.repeatUntil) { + body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`); + } + if (body.recurrence.length !== 0) { + body.recurrence = [`RRULE:${body.recurrence.join('')}`]; + } else { + delete body.recurrence; + } + responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/googleCalendar.png b/packages/nodes-base/nodes/Google/googleCalendar.png new file mode 100644 index 0000000000..2a2bfa1c7b Binary files /dev/null and b/packages/nodes-base/nodes/Google/googleCalendar.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 45d5012c8e..50eafb6ad6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -42,7 +42,8 @@ "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", - "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleApi.credentials.js", + "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -116,7 +117,8 @@ "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", - "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Gitlab/GitlabTrigger.node.js", + "dist/nodes/Google/GoogleCalendar.node.js", "dist/nodes/Google/GoogleDrive.node.js", "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", @@ -190,7 +192,8 @@ "@types/gm": "^1.18.2", "@types/imap-simple": "^4.2.0", "@types/jest": "^24.0.18", - "@types/lodash.set": "^4.3.6", + "@types/lodash.set": "^4.3.6", + "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.3.6", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", @@ -216,7 +219,8 @@ "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", + "lodash.unset": "^4.5.2", + "moment-timezone": "0.5.28", "mongodb": "^3.3.2", "mysql2": "^2.0.1", "n8n-core": "~0.20.0",