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 0000000000..59b632732d Binary files /dev/null and b/packages/nodes-base/nodes/Google/Contacts/googleContacts.png differ 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",