From 8027601283a77314ee395ed9d9ae4acedea7f67c Mon Sep 17 00:00:00 2001 From: trojanh Date: Tue, 28 Jan 2020 20:16:38 +0530 Subject: [PATCH] Add harvest TimeEntry CRUD API --- .../credentials/HarvestApi.credentials.ts | 25 + .../nodes/Harvest/ClientDescription.ts | 9 + .../nodes/Harvest/GenericFunctions.ts | 104 ++++ .../nodes-base/nodes/Harvest/Harvest.node.ts | 248 ++++++++ .../nodes/Harvest/TimeEntryDescription.ts | 528 ++++++++++++++++++ packages/nodes-base/nodes/Harvest/harvest.png | Bin 0 -> 2846 bytes packages/nodes-base/package.json | 2 + 7 files changed, 916 insertions(+) create mode 100644 packages/nodes-base/credentials/HarvestApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Harvest/ClientDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Harvest/Harvest.node.ts create mode 100644 packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/harvest.png diff --git a/packages/nodes-base/credentials/HarvestApi.credentials.ts b/packages/nodes-base/credentials/HarvestApi.credentials.ts new file mode 100644 index 0000000000..14a5ffac9d --- /dev/null +++ b/packages/nodes-base/credentials/HarvestApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HarvestApi implements ICredentialType { + name = 'harvestApi'; + displayName = 'Harvest API'; + properties = [ + { + displayName: 'Account ID', + name: 'accountId', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Visit your account details page, and grab the Account ID. See Harvest Personal Access Tokens.' + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Visit your account details page, and grab the Access Token. See Harvest Personal Access Tokens.' + }, + ]; +} diff --git a/packages/nodes-base/nodes/Harvest/ClientDescription.ts b/packages/nodes-base/nodes/Harvest/ClientDescription.ts new file mode 100644 index 0000000000..259ed3987d --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/ClientDescription.ts @@ -0,0 +1,9 @@ +import { INodeProperties } from "n8n-workflow"; + +export const clientOperations = [ + +] as INodeProperties[]; + +export const clientFields = [ + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/GenericFunctions.ts b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts new file mode 100644 index 0000000000..c465197c0b --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts @@ -0,0 +1,104 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function harvestApiRequest( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + qs: IDataObject = {}, + uri?: string, + body: IDataObject = {}, + option: IDataObject = {}, + ): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('harvestApi') as IDataObject; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + qs.access_token = credentials.accessToken; + qs.account_id = credentials.accountId; + // Convert to query string into a format the API can read + const queryStringElements: string[] = []; + for (const key of Object.keys(qs)) { + if (Array.isArray(qs[key])) { + (qs[key] as string[]).forEach(value => { + queryStringElements.push(`${key}=${value}`); + }); + } else { + queryStringElements.push(`${key}=${qs[key]}`); + } + } + + let options: OptionsWithUri = { + method, + body, + uri: `https://api.harvestapp.com/v2/${uri}?${queryStringElements.join('&')}`, + json: true, + headers: { + "User-Agent": "Harvest API" + } + }; + console.log({options}) + + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + const result = await this.helpers.request!(options); + console.log(result); + return result; + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Harvest credentials are not valid!'); + } + + if (error.error && error.error.error_summary) { + // Try to return the error prettier + throw new Error(`Harvest error response [${error.statusCode}]: ${error.error.error_summary}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function harvestApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + qs: IDataObject = {}, + uri?: string, + body: IDataObject = {}, + option: IDataObject = {}, + ): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + try { + do { + responseData = await harvestApiRequest.call(this, method, qs, uri, body, option); + qs.cursor = responseData.cursor.id; + returnData.push.apply(returnData, responseData.response); + } while ( + responseData.cursor.more === true && + responseData.cursor.hasNext === true + ); + return returnData; + } catch(error) { + throw error; + } +} diff --git a/packages/nodes-base/nodes/Harvest/Harvest.node.ts b/packages/nodes-base/nodes/Harvest/Harvest.node.ts new file mode 100644 index 0000000000..76123e9573 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -0,0 +1,248 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { harvestApiRequest } from './GenericFunctions'; +import { timeEntryOperations, timeEntryFields } from './TimeEntryDescription'; + + +export class Harvest implements INodeType { + description: INodeTypeDescription = { + displayName: 'Harvest', + name: 'harvest', + icon: 'file:harvest.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Access data on Harvest', + defaults: { + name: 'Harvest', + color: '#22BB44', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'harvestApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Time Entries', + value: 'timeEntry', + }, + ], + default: 'timeEntry', + description: 'The resource to operate on.', + }, + + // operations + ...timeEntryOperations, + + // fields + ...timeEntryFields + ] + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let endpoint = ''; + let requestMethod = ''; + let body: IDataObject | Buffer; + let qs: IDataObject; + + + for (let i = 0; i < items.length; i++) { + body = {}; + qs = {}; + + if (resource === 'timeEntry') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `time_entries/${id}`; + + try { + const responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'getAll') { + // ---------------------------------- + // getAll + // ---------------------------------- + + requestMethod = 'GET'; + + endpoint = 'time_entries'; + + const additionalFields = this.getNodeParameter('filters', i) as IDataObject; + const limit = this.getNodeParameter('limit', i) as string; + qs.per_page = limit; + Object.assign(qs, additionalFields); + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + } catch (error) { + throw error; + } + + } else if (operation === 'createByStartEnd') { + // ---------------------------------- + // createByStartEnd + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'time_entries'; + + const createFields = this.getNodeParameter('createFields', i) as IDataObject; + + Object.assign(qs, createFields); + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'createByDuration') { + // ---------------------------------- + // createByDuration + // ---------------------------------- + + requestMethod = 'POST'; + endpoint = 'time_entries'; + + const createFields = this.getNodeParameter('createFields', i) as IDataObject; + + Object.assign(qs, createFields); + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + requestMethod = 'DELETE'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `time_entries/${id}`; + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + } else if (operation === 'deleteExternal') { + // ---------------------------------- + // deleteExternal + // ---------------------------------- + + requestMethod = 'DELETE'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `time_entries/${id}/external_reference`; + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'restartTime') { + // ---------------------------------- + // restartTime + // ---------------------------------- + + requestMethod = 'PATCH'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `time_entries/${id}/restart`; + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'stopTime') { + // ---------------------------------- + // stopTime + // ---------------------------------- + + requestMethod = 'PATCH'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `time_entries/${id}/stop`; + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + requestMethod = 'PATCH'; + const id = this.getNodeParameter('id', i) as string; + endpoint = `time_entries/${id}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + Object.assign(qs, updateFields); + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } + +} diff --git a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts new file mode 100644 index 0000000000..39b3e07ca5 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts @@ -0,0 +1,528 @@ +import { INodeProperties } from "n8n-workflow"; + +export const timeEntryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + }, + }, + options: [ + { + name: 'Create via Duration', + value: 'createByDuration', + description: 'Create a time entry via duration', + }, + { + name: 'Create via Start and End Time', + value: 'createByStartEnd', + description: 'Create a time entry via start and end time', + }, + { + name: 'Delete', + value: 'delete', + description: `Delete a time entry`, + }, + { + name: 'Delete External Reference', + value: 'deleteExternal', + description: `Delete a time entry’s external reference.`, + }, + { + name: 'Get', + value: 'get', + description: 'Get data of a time entry', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all time entries', + }, + { + name: 'Restart', + value: 'restartTime', + description: 'Restart a time entry', + }, + { + name: 'Stop', + value: 'stopTime', + description: 'Stop a time entry', + }, + { + name: 'Update', + value: 'update', + description: 'Update a time entry', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const timeEntryFields = [ +/* -------------------------------------------------------------------------- */ +/* timeEntry:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your time entries.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', +}, +{ + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'User ID', + name: 'user_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the user with the given ID.', + }, + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'Is Billed', + name: 'is_billed', + type: 'boolean', + default: '', + description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.', + }, + { + displayName: 'Is Running', + name: 'is_running', + type: 'string', + default: '', + description: 'Pass true to only return running time entries and false to return non-running time entries.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'string', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', + }, + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or after the given date.', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + { + displayName: 'Page', + name: 'page', + type: 'string', + default: '', + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'The ID of the time entry you are retrieving.', +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:delete */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'The ID of the time entry you are deleting.', +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:deleteExternal */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'deleteExternal', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'The ID of the time entry whose external reference you are deleting.', +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:stopTime */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'stopTime', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'Stop a running time entry. Stopping a time entry is only possible if it’s currently running.', +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:restartTime */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'restartTime', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'Restart a stopped time entry. Restarting a time entry is only possible if it isn’t currently running.', +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:update */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'timeEntry', + ], + }, + }, + description: 'The ID of the time entry to update.', +}, +{ + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Project Id', + name: 'projectId', + type: 'string', + default: '', + description: 'The ID of the project to associate with the time entry.', + }, + { + displayName: 'Task Id', + name: 'taskId', + type: 'string', + default: '', + description: 'The ID of the task to associate with the time entry.', + }, + { + displayName: 'Spent Date', + name: 'spentDate', + type: 'dateTime', + default: '', + description: 'The ISO 8601 formatted date the time entry was spent.', + }, + { + displayName: 'Started Time', + name: 'startedTime', + type: 'string', + default: '', + description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', + }, + { + displayName: 'Ended Time', + name: 'endedTime', + type: 'string', + default: '', + description: 'The time the entry ended.', + }, + { + displayName: 'Hours', + name: 'hours', + type: 'string', + default: '', + description: 'The current amount of time tracked.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + } + ], +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:createByDuration */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Create Fields', + name: 'createFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'createByDuration', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Project Id', + name: 'projectId', + type: 'string', + default: '', + description: 'The ID of the project to associate with the time entry.', + }, + { + displayName: 'Task Id', + name: 'taskId', + type: 'string', + default: '', + description: 'The ID of the task to associate with the time entry.', + }, + { + displayName: 'Spent Date', + name: 'spentDate', + type: 'dateTime', + default: '', + description: 'The ISO 8601 formatted date the time entry was spent.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', + }, + { + displayName: 'Hours', + name: 'hours', + type: 'string', + default: '', + description: 'The current amount of time tracked.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + } + ], +}, + +/* -------------------------------------------------------------------------- */ +/* timeEntry:createByStartEnd */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Create Fields', + name: 'createFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'createByStartEnd', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Project Id', + name: 'projectId', + type: 'string', + default: '', + description: 'The ID of the project to associate with the time entry.', + }, + { + displayName: 'Task Id', + name: 'taskId', + type: 'string', + default: '', + description: 'The ID of the task to associate with the time entry.', + }, + { + displayName: 'Spent Date', + name: 'spentDate', + type: 'dateTime', + default: '', + description: 'The ISO 8601 formatted date the time entry was spent.', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', + }, + { + displayName: 'Started Time', + name: 'startedTime', + type: 'string', + default: '', + description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', + }, + { + displayName: 'Ended Time', + name: 'endedTime', + type: 'string', + default: '', + description: 'The time the entry ended.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + } + ], +}, + + + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/harvest.png b/packages/nodes-base/nodes/Harvest/harvest.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6713b91d0f63ca458ac1a014b848f0b12c845c GIT binary patch literal 2846 zcmV+(3*q#MP)0ssI2m!P+H000W*Nkl!-fl1w5!5LW7G2$OhZ8T;LJtauP&sT7NJn=DGV)UnANP?!ie z=~nncy{T3jFfE^ce1F!~S*IV^wJ0crW(I_lcOPpbu9MY(8ShG4O%2{@rDUN40_YD$bQfVpzwqx84GJysx%9ZPD};ZS)~Br06&e2a^&X3c3~(9EtwS{5B03Xl{2}t& z(|)!=7<^BI%y`vNaNKOZ#bLMQ-@azreuW|%%MG>l@npMQ&hIJa`XxS#u&*fR?syCKe=!J!-7laMnP!i4S~o`PHfKA~pS04W z4aSN%v?Tj(S35%V;Pc%F^TRT`K&#SZo*C|U%D;0Yw)tXROY5NKe>k`-4#+za%Jql{+Nk5$V^P5Mz?<@k?DX%^FQ z*@^v9vV*p_!QVBnbXGCv2 zftq9sl*fghFIDBZ{yyeT!IkSxf;@z#R?j5MA=`4I`t%V% zK+5jJDq%KGPIJseRFG@4gaF0&6kcbJbus`UlMVE-5qMyEIrvi06#}!Y92q}?_55Td zi`e|fdnV&ux?MBO3_L13aQyqCP+Mvj4yFC zdCWPkQa_IE^gH0WAU(dk2XPdcY{{h`GnOJpJ^)sT@hzJ;59Tk`Qp$ z|L)J@bC_*eReA?y3=eda;sJ}tMk2xx{l!IABE|LyI8|IN{Bse~jYtx5HHlCR*|(UZ zEG!e9&*R}R|6z)lS~nrmTRl8fkjPS=YWwj8UL{wG#cHyQRd-4P;To5vSezm?4aUDAfH!ut3{s4nqto#@(nEW%yKs<#gNae>$8k+v{m465&wa4Uvn#zj z`Ka{^hF@1W!NzS-(lZ0pyN*92sOO*gS=M;ZFz%LIIF4$5O2XxmI!% z$x-ZEJ)XrqioG5fV=N!UWRs-yu{B^A#LVWnc<+izK7U!9^0H36R=w0y^f)rO`CsNy z%r0M=>P6Rn^*lc*=2jAkf7OG8T`rO|H=<0S*g;BrB`jGzY2N;|JFTbKUF*TC=N!Mx zEysIP)>_qvzgyys0n&yT+#W!ksn9{b>GfK$o4tXKZmd&9#%RJEnT{0mI|Tfc@glt9 zO2V5IDWS;WGS&o^%iwfFBt4RXCa|!aE@EYzh#`)o=($#Wdefp_daTC+xz^W$Zy_k1 zW%sh_4Li<4rYXaHB1QR?fflz*FR};Ps-bKr8L=CpqsmYe*E*7ACnz{Cn4-gj%X*4k z_R0`KW`27PS|H%?ju%txAP0sJ1V-SZEXxoIMX$RD!$D%Sh~iu$@Nh^$ieC5O3st>H zlBj-_uCvrneIx*53K~aBEI2G2ylNaNMJYp!f!uHy36^TR3y_p!<0-~Jg-yN>)srI3 zZzS3^iYSU}joIbqQ|3_Xq+=UNMC)Q@61z3YjaqmflLb1=0^uFhD`ot;v`9Wm16?UOw zTJuJ22G2VBAHED?TKe<`PU9Dcy)?EdoM2yuk2pqO@7syEH`pLXg3M4>2gBy6pX zbV04|Y3F%1I}Zv+xKpvnV!IT5*2>W_fnv8vs~Ip&wXQW&1e{R|({G=C{8CghKg-cd zs$vDTy zA)IK{ly_V}*5mALwCQFv!67rgYq6hH%3n#taV{d{`9(pxL|OW0{2PSNMcxzdSVfhEj&YIBR6XO}WYt*sBw5!Qoroa=CfDaOBylb7mh;bvF1=1bw&2Kiy`38FPn;%CaEN_bQE%LTb w=eGbz>2tgJ&2z1{%XNh7TcjB14KFNz0V6-f+-Me@0{{R307*qoM6N<$f+hZq2><{9 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2c40a4d076..59bc312541 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -48,6 +48,7 @@ "dist/credentials/GitlabApi.credentials.js", "dist/credentials/GoogleApi.credentials.js", "dist/credentials/GumroadApi.credentials.js", + "dist/credentials/HarvestApi.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", "dist/credentials/HttpHeaderAuth.credentials.js", @@ -134,6 +135,7 @@ "dist/nodes/Google/GoogleSheets.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", + "dist/nodes/Harvest/Harvest.node.js", "dist/nodes/HtmlExtract/HtmlExtract.node.js", "dist/nodes/HttpRequest.node.js", "dist/nodes/Hubspot/Hubspot.node.js",