From eb9644f7de03569b4616879d1188ba4ae8190fe8 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 15 Aug 2020 17:25:15 -0400 Subject: [PATCH] :sparkles: GoogleContacts-Node (#775) * :sparkles: GoogleContacts-Node * :zap: small improvements * :zap: Add rawData field to get and getAll operations * :zap: Improvements * :zap: Small fix * :zap: Improvements * :zap: Small fixes --- .../GoogleContactsOAuth2Api.credentials.ts | 24 + .../Google/Contacts/ContactDescription.ts | 939 ++++++++++++++++++ .../nodes/Google/Contacts/GenericFunctions.ts | 174 ++++ .../Google/Contacts/GoogleContacts.node.ts | 310 ++++++ .../nodes/Google/Contacts/googleContacts.png | Bin 0 -> 4831 bytes packages/nodes-base/package.json | 2 + 6 files changed, 1449 insertions(+) create mode 100644 packages/nodes-base/credentials/GoogleContactsOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Google/Contacts/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Google/Contacts/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts create mode 100644 packages/nodes-base/nodes/Google/Contacts/googleContacts.png diff --git a/packages/nodes-base/credentials/GoogleContactsOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GoogleContactsOAuth2Api.credentials.ts new file mode 100644 index 0000000000..bab616fd77 --- /dev/null +++ b/packages/nodes-base/credentials/GoogleContactsOAuth2Api.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/contacts', +]; + +export class GoogleContactsOAuth2Api implements ICredentialType { + name = 'googleContactsOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Google Contacts OAuth2 API'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Contacts/ContactDescription.ts b/packages/nodes-base/nodes/Google/Contacts/ContactDescription.ts new file mode 100644 index 0000000000..b1e3a45bc0 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Contacts/ContactDescription.ts @@ -0,0 +1,939 @@ +import { + INodeProperties, +} from 'n8n-workflow'; +import { LoadNodeParameterOptions } from 'n8n-core'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all contacts', + }, + ], + default: 'create', + description: 'The operation to perform.' + } +] as INodeProperties[]; + +export const contactFields = [ + /* -------------------------------------------------------------------------- */ + /* contact:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Family Name', + name: 'familyName', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'Given Name', + name: 'givenName', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Addresses', + name: 'addressesUi', + placeholder: 'Add Address', + type: 'fixedCollection', + default: {}, + options: [ + { + displayName: 'Address', + name: 'addressesValues', + values: [ + { + displayName: 'Street Address', + name: 'streetAddress', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City', + }, + { + displayName: 'Region', + name: 'region', + type: 'string', + default: '', + description: 'Region', + }, + { + displayName: 'Country Code', + name: 'countryCode', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + description: 'Postal code', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Work', + value: 'work', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Birthday', + name: 'birthday', + type: 'dateTime', + default: '', + }, + { + displayName: 'Company', + name: 'companyUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Company', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'companyValues', + displayName: 'Company', + values: [ + { + displayName: 'Current', + name: 'current', + type: 'boolean', + default: false, + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Emails', + name: 'emailsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Email', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'emailsValues', + displayName: 'Email', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Work', + value: 'work', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + description: `The type of the email address. The type can be custom or one of these predefined values`, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The email address.', + }, + ], + }, + ], + }, + { + displayName: 'Events', + name: 'eventsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Event', + description: 'An event related to the person.', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'eventsValues', + displayName: 'Event', + values: [ + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + description: 'The date of the event.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Anniversary', + value: 'anniversary', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + description: `The type of the event. The type can be custom or one of these predefined values`, + }, + ], + }, + ], + }, + { + displayName: 'File As', + name: 'fileAs', + type: 'string', + default: '', + description: 'The name that should be used to sort the person in a list.', + }, + { + displayName: 'Group', + name: 'group', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: [], + }, + { + displayName: 'Middle Name', + name: 'middleName', + type: 'string', + default: '', + }, + { + displayName: 'Notes', + name: 'biographies', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Phone', + name: 'phoneUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Phone', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'phoneValues', + displayName: 'Phone', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Home', + value: 'home', + }, + { + name: 'Work', + value: 'work', + }, + { + name: 'Mobile', + value: 'mobile', + }, + { + name: 'Home Fax', + value: 'homeFax', + }, + { + name: 'Work Fax', + value: 'workFax', + }, + { + name: 'Other Fax', + value: 'otherFax', + }, + { + name: 'Pager', + value: 'pager', + }, + { + name: 'Work Mobile', + value: 'workMobile', + }, + { + name: 'Work Pager', + value: 'workPager', + }, + { + name: 'Main', + value: 'main', + }, + { + name: 'Google Voice', + value: 'googleVoice', + }, + { + name: 'Other', + value: 'other', + }, + ], + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The phone number.', + }, + ], + }, + ], + }, + { + displayName: 'Relations', + name: 'relationsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Relation', + typeOptions: { + multipleValues: true + }, + options: [ + { + name: 'relationsValues', + displayName: 'Relation', + values: [ + { + displayName: 'Person', + name: 'person', + type: 'string', + default: '', + description: 'The name of the other person this relation refers to.', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Spouse', + value: 'spouse', + }, + { + name: 'Child', + value: 'child', + }, + { + name: 'Mother', + value: 'mother', + }, + { + name: 'Father', + value: 'father', + }, + { + name: 'Parent', + value: 'parent', + }, + { + name: 'Brother', + value: 'brother', + }, + { + name: 'Sister', + value: 'sister', + }, + { + name: 'Friend', + value: 'friend', + }, + { + name: 'Relative', + value: 'relative', + }, + { + name: 'Domestic Partner', + value: 'domesticPartner', + }, + { + name: 'Manager', + value: 'manager', + }, + { + name: 'Assistant', + value: 'assistant', + }, + { + name: 'Referred By', + value: 'referredBy', + }, + ], + default: '', + description: `The person's relation to the other person. The type can be custom or one of these predefined values`, + }, + ], + }, + ], + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'Addresses', + value: 'addresses', + }, + { + name: 'Biographies', + value: 'biographies', + }, + { + name: 'Birthdays', + value: 'birthdays', + }, + { + name: 'Cover Photos', + value: 'coverPhotos', + }, + { + name: 'Email Addresses', + value: 'emailAddresses', + }, + { + name: 'Events', + value: 'events', + }, + { + name: 'Genders', + value: 'genders', + }, + { + name: 'IM Clients', + value: 'imClients', + }, + { + name: 'Interests', + value: 'interests', + }, + { + name: 'Locales', + value: 'locales', + }, + { + name: 'Memberships', + value: 'memberships', + }, + { + name: 'Metadata', + value: 'metadata', + }, + { + name: 'Names', + value: 'names', + }, + { + name: 'Nicknames', + value: 'nicknames', + }, + { + name: 'Occupations', + value: 'occupations', + }, + { + name: 'Organizations', + value: 'organizations', + }, + { + name: 'Phone Numbers', + value: 'phoneNumbers', + }, + { + name: 'Photos', + value: 'photos', + }, + { + name: 'Relations', + value: 'relations', + }, + { + name: 'Residences', + value: 'residences', + }, + { + name: 'Sip Addresses', + value: 'sipAddresses', + }, + { + name: 'Skills', + value: 'skills', + }, + { + name: 'URLs', + value: 'urls', + }, + { + name: 'User Defined', + value: 'userDefined', + }, + ], + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + description: 'A field mask to restrict which fields on each person are returned. Multiple fields can be specified by separating them with commas.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + 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: [ + 'contact', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'Addresses', + value: 'addresses', + }, + { + name: 'Biographies', + value: 'biographies', + }, + { + name: 'Birthdays', + value: 'birthdays', + }, + { + name: 'Cover Photos', + value: 'coverPhotos', + }, + { + name: 'Email Addresses', + value: 'emailAddresses', + }, + { + name: 'Events', + value: 'events', + }, + { + name: 'Genders', + value: 'genders', + }, + { + name: 'IM Clients', + value: 'imClients', + }, + { + name: 'Interests', + value: 'interests', + }, + { + name: 'Locales', + value: 'locales', + }, + { + name: 'Memberships', + value: 'memberships', + }, + { + name: 'Metadata', + value: 'metadata', + }, + { + name: 'Names', + value: 'names', + }, + { + name: 'Nicknames', + value: 'nicknames', + }, + { + name: 'Occupations', + value: 'occupations', + }, + { + name: 'Organizations', + value: 'organizations', + }, + { + name: 'Phone Numbers', + value: 'phoneNumbers', + }, + { + name: 'Photos', + value: 'photos', + }, + { + name: 'Relations', + value: 'relations', + }, + { + name: 'Residences', + value: 'residences', + }, + { + name: 'Sip Addresses', + value: 'sipAddresses', + }, + { + name: 'Skills', + value: 'skills', + }, + { + name: 'URLs', + value: 'urls', + }, + { + name: 'User Defined', + value: 'userDefined', + }, + ], + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + default: '', + description: 'A field mask to restrict which fields on each person are returned. Multiple fields can be specified by separating them with commas.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'contact', + ], + }, + }, + options: [ + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Last Modified Ascending', + value: 'LAST_MODIFIED_ASCENDING', + description: 'Sort people by when they were changed; older entries first.', + }, + { + name: 'Last Modified Descending', + value: 'LAST_MODIFIED_DESCENDING', + description: 'Sort people by when they were changed; newer entries first.', + }, + { + name: 'First Name Ascending', + value: 'FIRST_NAME_ASCENDING', + description: 'Sort people by first name.', + }, + { + name: 'Last Name Ascending', + value: 'LAST_NAME_ASCENDING', + description: 'Sort people by last name.', + }, + ], + default: '', + description: 'The order of the contacts returned in the result.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Google/Contacts/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Contacts/GenericFunctions.ts new file mode 100644 index 0000000000..950c261262 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Contacts/GenericFunctions.ts @@ -0,0 +1,174 @@ +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://people.googleapis.com/v1${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.requestOAuth2.call(this, 'googleContactsOAuth2Api', options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errors; + + if (error.response.body.error.errors) { + + errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message).join('|'); + + } else { + errors = error.response.body.error.message; + } + + // Try to return the error prettier + throw new Error( + `Google Contacts error response [${error.statusCode}]: ${errors}` + ); + } + 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.pageSize = 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; +} + +export const allFields = [ + 'addresses', + 'biographies', + 'birthdays', + 'coverPhotos', + 'emailAddresses', + 'events', + 'genders', + 'imClients', + 'interests', + 'locales', + 'memberships', + 'metadata', + 'names', + 'nicknames', + 'occupations', + 'organizations', + 'phoneNumbers', + 'photos', + 'relations', + 'residences', + 'sipAddresses', + 'skills', + 'urls', + 'userDefined', +]; + +export function cleanData(responseData: any) { + const fields = ['emailAddresses', 'phoneNumbers', 'relations', 'events', 'addresses']; + const newResponseData = []; + if (!Array.isArray(responseData)) { + responseData = [responseData]; + } + for (let y = 0; y < responseData.length; y++ ) { + const object: { [key: string]: any } = {}; + for (const key of Object.keys(responseData[y])) { + if (key === 'metadata') { + continue; + } + if (key === 'photos') { + responseData[y][key] = responseData[y][key].map(((photo: IDataObject) => photo.url)); + } + if (key === 'names') { + delete responseData[y][key][0].metadata; + responseData[y][key] = responseData[y][key][0]; + } + if (key === 'memberships') { + for (let i = 0; i < responseData[y][key].length; i++) { + responseData[y][key][i] = responseData[y][key][i].metadata.source.id; + } + } + if (key === 'birthdays') { + for (let i = 0; i < responseData[y][key].length; i++) { + const { year, month, day } = responseData[y][key][i].date; + responseData[y][key][i] = `${month}/${day}/${year}`; + } + responseData[y][key] = responseData[y][key][0]; + } + if (key === 'userDefined' || key === 'organizations' || key === 'biographies') { + for (let i = 0; i < responseData[y][key].length; i++) { + delete responseData[y][key][i].metadata; + } + } + if (fields.includes(key)) { + const value: { [key: string]: any } = {}; + for (const data of responseData[y][key]) { + let result; + if (value[data.type] === undefined) { + value[data.type] = []; + } + + if (key === 'relations') { + result = data.person; + } else if (key === 'events') { + const { year, month, day } = data.date; + result = `${month}/${day}/${year}`; + } else if (key === 'addresses') { + delete data.metadata; + result = data; + } else { + result = data.value; + } + value[data.type].push(result); + delete data.type; + } + if (Object.keys(value).length > 0) { + object[key] = value; + } + } else { + object[key] = responseData[y][key]; + } + } + newResponseData.push(object); + } + return newResponseData; +} diff --git a/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts b/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts new file mode 100644 index 0000000000..313c68da09 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Contacts/GoogleContacts.node.ts @@ -0,0 +1,310 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; + +import { + allFields, + cleanData, + googleApiRequest, + googleApiRequestAllItems, +} from './GenericFunctions'; + +import { + contactOperations, + contactFields, +} from './ContactDescription'; + +import * as moment from 'moment'; + +export class GoogleContacts implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Contacts', + name: 'googleContacts', + icon: 'file:googleContacts.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Google Contacts API.', + defaults: { + name: 'Google Contacts', + color: '#1a73e8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'googleContactsOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + description: 'The resource to operate on.' + }, + ...contactOperations, + ...contactFields, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getGroups( + this: ILoadOptionsFunctions + ): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await googleApiRequestAllItems.call( + this, + 'contactGroups', + 'GET', + `/contactGroups`, + ); + for (const group of groups) { + const groupName = group.name; + const groupId = group.resourceName; + returnData.push({ + name: groupName, + value: groupId + }); + } + 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 === 'contact') { + //https://developers.google.com/calendar/v3/reference/events/insert + if (operation === 'create') { + const familyName = this.getNodeParameter('familyName', i) as string; + const givenName = this.getNodeParameter('givenName', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + names: [ + { + familyName, + givenName, + middleName: '', + }, + ], + }; + + if (additionalFields.middleName) { + //@ts-ignore + body.names[0].middleName = additionalFields.middleName as string; + } + + if (additionalFields.companyUi) { + const companyValues = (additionalFields.companyUi as IDataObject).companyValues as IDataObject[]; + body.organizations = companyValues; + } + + if (additionalFields.phoneUi) { + const phoneValues = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[]; + body.phoneNumbers = phoneValues; + } + + if (additionalFields.addressesUi) { + const addressesValues = (additionalFields.addressesUi as IDataObject).addressesValues as IDataObject[]; + body.addresses = addressesValues; + } + + if (additionalFields.relationsUi) { + const relationsValues = (additionalFields.relationsUi as IDataObject).relationsValues as IDataObject[]; + body.relations = relationsValues; + } + + if (additionalFields.eventsUi) { + const eventsValues = (additionalFields.eventsUi as IDataObject).eventsValues as IDataObject[]; + for (let i = 0; i < eventsValues.length; i++) { + const [month, day, year] = moment(eventsValues[i].date as string).format('MM/DD/YYYY').split('/'); + eventsValues[i] = { + date: { + day, + month, + year, + }, + type: eventsValues[i].type, + }; + } + body.events = eventsValues; + } + + if (additionalFields.birthday) { + const [month, day, year] = moment(additionalFields.birthday as string).format('MM/DD/YYYY').split('/'); + + body.birthdays = [ + { + date: { + day, + month, + year + } + } + ]; + } + + if (additionalFields.emailsUi) { + const emailsValues = (additionalFields.emailsUi as IDataObject).emailsValues as IDataObject[]; + body.emailAddresses = emailsValues; + } + + if (additionalFields.biographies) { + body.biographies = [ + { + value: additionalFields.biographies, + contentType: 'TEXT_PLAIN', + }, + ]; + } + + if (additionalFields.customFieldsUi) { + const customFieldsValues = (additionalFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + body.userDefined = customFieldsValues; + } + + if (additionalFields.group) { + const memberships = (additionalFields.group as string[]).map((groupId: string) => { + return { + contactGroupMembership: { + contactGroupResourceName: groupId + } + }; + }); + + body.memberships = memberships; + } + + responseData = await googleApiRequest.call( + this, + 'POST', + `/people:createContact`, + body, + qs + ); + + responseData.contactId = responseData.resourceName.split('/')[1]; + + } + //https://developers.google.com/people/api/rest/v1/people/deleteContact + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + responseData = await googleApiRequest.call( + this, + 'DELETE', + `/people/${contactId}:deleteContact`, + {} + ); + responseData = { success: true }; + } + //https://developers.google.com/people/api/rest/v1/people/get + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + const fields = this.getNodeParameter('fields', i) as string[]; + const rawData = this.getNodeParameter('rawData', i) as boolean; + + if (fields.includes('*')) { + qs.personFields = allFields.join(','); + } else { + qs.personFields = (fields as string[]).join(','); + } + + responseData = await googleApiRequest.call( + this, + 'GET', + `/people/${contactId}`, + {}, + qs, + ); + + if (!rawData) { + responseData = cleanData(responseData)[0]; + } + + responseData.contactId = responseData.resourceName.split('/')[1]; + } + //https://developers.google.com/people/api/rest/v1/people.connections/list + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const fields = this.getNodeParameter('fields', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + const rawData = this.getNodeParameter('rawData', i) as boolean; + + if (options.sortOrder) { + qs.sortOrder = options.sortOrder as number; + } + + if (fields.includes('*')) { + qs.personFields = allFields.join(','); + } else { + qs.personFields = (fields as string[]).join(','); + } + + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'connections', + 'GET', + `/people/me/connections`, + {}, + qs, + ); + } else { + qs.pageSize = this.getNodeParameter('limit', i) as number; + responseData = await googleApiRequest.call( + this, + 'GET', + `/people/me/connections`, + {}, + qs, + ); + responseData = responseData.connections; + } + + if (!rawData) { + responseData = cleanData(responseData); + } + + for (let i = 0; i < responseData.length; i++ ) { + responseData[i].contactId = responseData[i].resourceName.split('/')[1]; + } + } + } + } + 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/Contacts/googleContacts.png b/packages/nodes-base/nodes/Google/Contacts/googleContacts.png new file mode 100644 index 0000000000000000000000000000000000000000..59b632732df0ee77f34bf6cae454dd3ca5650142 GIT binary patch literal 4831 zcmY*dcRXBMw4TwfXwiu}A&4+TbYl!*5Iy=J>KLMz(QAn4(ItA6iA3*R5WOdQ5WV*n zL_$37&Aa!#eSYh#Z?FBWZ|$|uUuTCuQBxo#Vjuzl0HlhK5t`Q?c4LGD*Kg>>l*_dP zVl)+G03}0Ao7amThl2wE zgjxge{%{Pg{mpoMZP(d^1VMXIVmQI7TyGYdy^G{n>1=_U&R^MqbQd$g+=$kX1=0R#1vWc!-|y~a0T zJ~q(b6jxhGHl*qkkgTH%8YB!6g7CA!i9jF_%*DbIs)>;QlYTvuWP9f7>ICKE^YHM1 zcnCrqU99*Xii?Z$@eA+?2!O8{V2qc8tC=U*0mJ@}$p7jf&={19wUev0qXXzh*Ua3} z%~g_(?WWMb1mw1mpYn*x*DeZCv^Q0C|ie;*s_<{8dBZsHbQe_*KSK z%9P&OxO-!1b9O6^X53@DtoWeVg1ekGAqTMr_??MZFr@}n2%eqhvv5i|5Uh$EFKGH% z7$s@q!N`+B(>R%zZSEe8vkqq(?jDxzYrp;H>(A7z-d8pX&fqIvR0?AD13m`~o&U!C z#(cg)va{-!%P*NHUKX35Og6T^JKVz;;{BcRZEdt$pX$r~POcMTzy$10e4U$`f$R~e zG)qe@HM<)$v(DAY$MRRJAv01a!7=40sb3}AqIMdUyFn4pD*!)ZMW3HnZkCie)28#N zgVh7(O)+ov_Aj1BV_JX<5xpg6kBiEYeKpE5;dNuK#o4p8@}g(RJapEvohY@jb(B7L zi4mDXh9jF!^ewsPw;TS_-0Ty1(USOeEEK|Yj_Yg@Q(e*Di}i4usA+;YC*XVSxz*U6 zUB(aXj}`5(L2!oUV%Oq2?2)!h1Vmv2GhZaT!;||17nS%we#uWJEi^1g<(iN3W2><* z{sO4E`?`=YZjraCe+};MOdS3p|1FLkLTJ+3-u*3c)@vg(&lpBd^2XVvx+Iv7h^L-+ z_CU?`O7i`^sj}7A*#v0}MvfiEj(yv-g%#mI1IQw{Ft*gwH5RO$aIp1+pf zJ>pS9GF9QYf_v4K5ziW&iaVeT(_aJ^=@}0^>wFy#$vZ zh>zzbIQ3^!7cAy-;ym?gHjGjVgl^yw!@J^5dJvX!Dq=*1XU(K<>p%J!9+SEBkH4ee z!``~5qUuLZY4ADw`R_8 z+?8L-OY<&~k%I4Z*tBKpxY@CV`D#lakL$WoT-b$7gpgT_%{A=pzKXHIbL`^p3Ew&| z_WCR>;Cu*HE(51|H!q9DF^A=tiHf&oc1eU#953T23v(;yx&}j&Dfh)Br!|x&Rcw3~U)udjm_ub$7ksF0WK{!Fd#vKN_&CS`oOU}Pqd{}M z7okd+Oc7g}x%Ntu&Je}2LN0gD@*qsD>j33XTfK?UbfY@UUVD{6r)9MTk7L2_E|Z5N zmS3m+GKjNi@p5Q;gi$B|6Nv{}DD4%_m;2-~X)xWF_?2Nm=hD zWG0OKNWLYBn1T6xaf?X5nU}L#XGbb7@U~!um5Gc)fF;6s$9c5~$}X84f(I!@$OTYD zFn1pCcu-ybQemsiIkqbH1Li#+bS6ec&<%J4+&^j!OP9IY<2@f~)f9^2Jf@hN6SF~I zTAO)!qdq?M_D>opCRWA{Y+Seei!{>@R|tz{tYNkam!44!&{2p+)t#oJMRaR2S0WVO zml`iGKNo*1w^$TP1zhX8Pi<7OLv2vK1iqXO)rAYs^e|73^)WqHZeO`G5SYw+)NVU* zI2H2<#z9o;z1u>hWfDTp>EH1yRZ-1JAi+MGrpcP4kK$&NbSm-%p_&ZL$TOw@dXE4zTsORd($)D;nM!M!91df}-XcED?i@{VWT@ z>Wg%W>_Ry{;LDlG``Z)y%y42HU2~>|?niapMr_`S<~#CD-Vj?tMV`_9^>BKeG*m^yw5d#RKihvyAV7r^~=zDu%bz<-?F=EY8uP zgN3dux}R~`?~Dfd>t_)=G!VGuxOg?tHlcZndU_WT^0eTR*b{Y@q=4Uz3f8h=T&T?` zi-nqtgH(;wmqPP+;F%jibzB#eGSyKzFGys9(srV4<4S}|j;KR85tPiWvuQGr_;aL0 zdlYd}iQb*4U8$aI%*M5JStOT+{#+C$ej68Z^HE3pYZ8qPR>>p7wFTyC@sk!!@p9GI z#F3)%$&1^Bh*7?sd3xVEp19Y7HV;+?T`SYiUQD9<-$)#FJdR*w{3Znw{5}iL)0iBq;9K`fIBeZQfP7T*nw(F87}8i4G5VW&nUR0T@#tUkY^PAM+3&3 zpK2SWMXV(`#$ff6(C?+#$J~Ad#U1)s@%dfmcKpLv=e_E8Ds*=E%SfV^rpV>VIZ!g6 z?s(vGc2|CZM;MBr)iS7_RT(*(SoG~U!`vp`6}#orM!dmzPj&`S@ltYB??|NY@mC{O z>PhmKB3_z_7gwvK#>$Ori&LIhT_|SVL6kBmXb5GWU?GXIHqrmj85%;tL_+SHki}VHyZuf%y7x{{LLg8v8TioCe1?>nEA|}aLnGcy# zr7C+DHc2IyPAk;Ca9Q4D*oAEQd61Z4PI(o*tvmjkxs9WJqKgA&iN|L_-*x&wx;YN* z)I~hh!p{ZLp4Daec&q5vzQhtcJ}*k*q{hZ-Q#37mpnIRw*dLa(u~70noO3r@XZ}@j z7j)o$TG@dQq2`yDu5KaHkN8Dvs#g}FcuSdgsb|Fd!)D76F{Kqxwgi7&yMjOmc06ME zM?&aFb?(U*9z%Y(w>`}AsiYvZDw1aUa4)(!trEp^wfK`HSlgXCbeniJnmf#(9u@Ja z=NQgO!J6B>oG$Q`bwQk=kY|n_S9I`0qgQ!!uz`Uf+9j^6EE8NPly9%k ztPoX;MJkT^>+HqE3mTsL-`=Fq*KVa^_h58O@;hqwMPJ1fdT`b_h8|@zO>52)7w<2p z5K%jQVOc(@Ycs;niysJNZRLF?6c>+76r>VL&AyE<-`DivWX9@t&z`KcewfEyF1nYI zTM@_78qe(arK&om&X-Xr#@+ZV=Q8kcQ$2BLH`5NoM7qF~F7cr}e@eiaX{qnocX<6_ zUnRD<-k^`ZE$LY7G%%UXUlQ^3_Z`^l`1wogw@5h2r!ksIjloOC$@bb^*cOr+G9H{6^HaTVaH-4$0)p93!wZwOBeTSdeQI&{PuYzs zent_cM1B}7;*RkGVfsu~BfHq8Pgj0Z4Ac~0&#O2*VfOS!C7>D`FwB93&CUwH3b9rv0ye-49C?YI^ zEj%AV-GpQK@S$`#{ZPKc+WI*j+2x1vCGYw?;yBNstHkXL;)>$h$+G>Ax-g zIM^BW902+Few{H4IeGD$w+7c`bFl#ix3m8$0cTpwKCI=pYCBx)>i*zc0%_vQ!=sGI z9vs2jx7Mw4U@G#EW#4`{)iB3hYA6$1CPvbZHmN2v?oX|9`XO!H{CJndf3dp%9mil4 zIkT5^6G1JKk#YCxHuAEqd^416Zq}VTY3ga<|oWm0QmS7WyiPJ+RSx}808e7oSdF75hS7&Kp zIfoCq{hmcO_B%iHYH2()sn1xxlvOM#qKy5C4cr(pT&qdmn)hTIiAdSSVd8ZRDxGNP zvWbZBDe9d9e)DLLXZ2;e>Pjd)C3)H|cg6QxqtH|j==;rM&hYX~d*W@^E3+<+(my}@ f)E4mFzAKy!GWO*k+YKRgU)1dzW&6C6O literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a5c38a2ef2..4b6825388f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -69,6 +69,7 @@ "dist/credentials/GitlabOAuth2Api.credentials.js", "dist/credentials/GoogleApi.credentials.js", "dist/credentials/GoogleCalendarOAuth2Api.credentials.js", + "dist/credentials/GoogleContactsOAuth2Api.credentials.js", "dist/credentials/GoogleDriveOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", @@ -231,6 +232,7 @@ "dist/nodes/Gitlab/Gitlab.node.js", "dist/nodes/Gitlab/GitlabTrigger.node.js", "dist/nodes/Google/Calendar/GoogleCalendar.node.js", + "dist/nodes/Google/Contacts/GoogleContacts.node.js", "dist/nodes/Google/Drive/GoogleDrive.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", "dist/nodes/Google/Task/GoogleTasks.node.js",