From 8027601283a77314ee395ed9d9ae4acedea7f67c Mon Sep 17 00:00:00 2001 From: trojanh Date: Tue, 28 Jan 2020 20:16:38 +0530 Subject: [PATCH 1/4] 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 000000000..14a5ffac9 --- /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 000000000..259ed3987 --- /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 000000000..c465197c0 --- /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 000000000..76123e957 --- /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 000000000..39b3e07ca --- /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 2c40a4d07..59bc31254 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", From 7b50a4bcfdc03244cbd0074615041ab4ee7276c5 Mon Sep 17 00:00:00 2001 From: trojanh Date: Tue, 28 Jan 2020 21:20:15 +0530 Subject: [PATCH 2/4] Add other APIs --- .../nodes/Harvest/ClientDescription.ts | 127 +++++++ .../nodes/Harvest/CompanyDescription.ts | 26 ++ .../nodes/Harvest/ContactDescription.ts | 136 +++++++ .../nodes/Harvest/ExpenseDescription.ts | 178 +++++++++ .../nodes-base/nodes/Harvest/Harvest.node.ts | 352 ++++++++++++++++++ .../nodes/Harvest/InvoiceDescription.ts | 189 ++++++++++ .../nodes/Harvest/ProjectDescription.ts | 151 ++++++++ .../nodes/Harvest/TaskDescription.ts | 143 +++++++ .../nodes/Harvest/UserDescription.ts | 148 ++++++++ 9 files changed, 1450 insertions(+) create mode 100644 packages/nodes-base/nodes/Harvest/CompanyDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/ContactDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/ExpenseDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/InvoiceDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/ProjectDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/TaskDescription.ts create mode 100644 packages/nodes-base/nodes/Harvest/UserDescription.ts diff --git a/packages/nodes-base/nodes/Harvest/ClientDescription.ts b/packages/nodes-base/nodes/Harvest/ClientDescription.ts index 259ed3987..1cefde7da 100644 --- a/packages/nodes-base/nodes/Harvest/ClientDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ClientDescription.ts @@ -1,9 +1,136 @@ import { INodeProperties } from "n8n-workflow"; export const clientOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'client', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a client', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all clients', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, ] as INodeProperties[]; export const clientFields = [ +/* -------------------------------------------------------------------------- */ +/* client:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your clients.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'client', + ], + 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: [ + 'client', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Is Active', + name: 'is_active', + type: 'boolean', + default: '', + description: 'Pass true to only return active clients and false to return inactive clients.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return clients that have been updated since the given date and time.', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* client:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'client', + ], + }, + }, + description: 'The ID of the client you are retrieving.', +} + ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts new file mode 100644 index 000000000..6b85e78bc --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts @@ -0,0 +1,26 @@ +import { INodeProperties } from "n8n-workflow"; + +export const clientOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'company', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieves the company for the currently authenticated user', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/ContactDescription.ts b/packages/nodes-base/nodes/Harvest/ContactDescription.ts new file mode 100644 index 000000000..aa926d233 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/ContactDescription.ts @@ -0,0 +1,136 @@ +import { INodeProperties } from "n8n-workflow"; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all contacts', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const contactFields = [ + +/* -------------------------------------------------------------------------- */ +/* contact:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'contact', + ], + 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: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Is Active', + name: 'is_active', + type: 'boolean', + default: '', + description: 'Pass true to only return active clients and false to return inactive clients.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return clients that have been updated since the given date and time.', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* contact:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'contact', + ], + }, + }, + description: 'The ID of the contact you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts new file mode 100644 index 000000000..609a93f6b --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts @@ -0,0 +1,178 @@ +import { INodeProperties } from "n8n-workflow"; + +export const expenseOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'expense', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a expense', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all expenses', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const expenseFields = [ + +/* -------------------------------------------------------------------------- */ +/* expense:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'expense', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your expenses.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'expense', + ], + 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: [ + 'expense', + ], + 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: 'Project ID', + name: 'project_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: '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)', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* expense:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'expense', + ], + }, + }, + description: 'The ID of the expense you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/Harvest.node.ts b/packages/nodes-base/nodes/Harvest/Harvest.node.ts index 76123e957..4d42ec607 100644 --- a/packages/nodes-base/nodes/Harvest/Harvest.node.ts +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -43,6 +43,26 @@ export class Harvest implements INodeType { name: 'Time Entries', value: 'timeEntry', }, + { + name: "Client", + value: "client" + }, + { name: "Project", + value: "project"}, + { name: "Contact", + value: "contact"}, + { name: "Company", + value: "company"}, + { name: "Invoice", + value: "invoice"}, + { name: "Task", + value: "task"}, + { name: "User", + value: "user"}, + { name: "Expense", + value: "expense"}, + { name: "Estimates", + value: "estimate"} ], default: 'timeEntry', description: 'The resource to operate on.', @@ -50,6 +70,7 @@ export class Harvest implements INodeType { // operations ...timeEntryOperations, + ...compa // fields ...timeEntryFields @@ -237,6 +258,337 @@ export class Harvest implements INodeType { throw new Error(`The operation "${operation}" is not known!`); } + } else if (resource === 'client') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `clients/${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 = 'clients'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'project') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `projects/${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 = 'projects'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'user') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `users/${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 = 'users'; + + 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 === 'me') { + // ---------------------------------- + // getAll + // ---------------------------------- + + requestMethod = 'GET'; + + endpoint = 'users/me'; + + try { + let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push.apply(responseData); + } catch (error) { + throw error; + } + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'contact') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `contacts/${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 = 'contacts'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'company') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `company`; + + try { + const responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + returnData.push(responseData); + } catch (error) { + throw error; + } + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'task') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `tasks/${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 = 'tasks'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'invoice') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `invoices/${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 = 'invoices'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'expense') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `expenses/${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 = 'expenses'; + + 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 { + throw new Error(`The resource "${resource}" is not known!`); + } } else { throw new Error(`The resource "${resource}" is not known!`); } diff --git a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts new file mode 100644 index 000000000..edc710afe --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts @@ -0,0 +1,189 @@ +import { INodeProperties } from "n8n-workflow"; + +export const invoiceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a invoice', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all invoices', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const invoiceFields = [ + +/* -------------------------------------------------------------------------- */ +/* invoice:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your invoices.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'invoice', + ], + 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: [ + 'invoice', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'Project ID', + name: 'project_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + 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: 'State', + name: 'state', + type: 'multiOptions', + options: [ + { + name: 'draft', + value: 'draft', + }, + { + name: 'open', + value: 'open', + }, + { + name: 'paid', + value: 'paid', + }, + { + name: 'closed', + value: 'closed', + }, + ], + default: [], + description: 'Only return invoices with a state matching the value provided. Options: draft, open, paid, or closed.', + }, + { + 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)', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* invoice:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'invoice', + ], + }, + }, + description: 'The ID of the invoice you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/ProjectDescription.ts b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts new file mode 100644 index 000000000..3cbc6536f --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts @@ -0,0 +1,151 @@ +import { INodeProperties } from "n8n-workflow"; + +export const projectOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'project', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a project', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all projects', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const projectFields = [ + +/* -------------------------------------------------------------------------- */ +/* projects:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your projects.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'project', + ], + 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: [ + 'project', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Is Active', + name: 'is_active', + type: 'string', + default: '', + description: 'Pass true to only return active projects and false to return inactive projects.', + }, + { + displayName: 'Client Id', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return projects belonging to the client with the given ID.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'string', + default: '', + description: 'Only return projects by updated_since.', + }, + { + displayName: 'Page', + name: 'page', + type: 'string', + default: '', + description: 'The page number to use in pagination.', + }, + + ] +}, + +/* -------------------------------------------------------------------------- */ +/* project:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'project', + ], + }, + }, + description: 'The ID of the project you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/TaskDescription.ts b/packages/nodes-base/nodes/Harvest/TaskDescription.ts new file mode 100644 index 000000000..e1c3020ea --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/TaskDescription.ts @@ -0,0 +1,143 @@ +import { INodeProperties } from "n8n-workflow"; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of a task', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all tasks', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const taskFields = [ + +/* -------------------------------------------------------------------------- */ +/* task:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your tasks.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'task', + ], + 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: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Is Active', + name: 'is_active', + type: 'string', + default: '', + description: 'Pass true to only return active tasks and false to return inactive tasks.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'string', + default: '', + description: 'Only return tasks belonging to the task with the given ID.', + }, + { + displayName: 'Page', + name: 'page', + type: 'string', + default: '', + description: 'The page number to use in pagination.', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* task:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'task', + ], + }, + }, + description: 'The ID of the task you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/UserDescription.ts b/packages/nodes-base/nodes/Harvest/UserDescription.ts new file mode 100644 index 000000000..d78421e09 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/UserDescription.ts @@ -0,0 +1,148 @@ +import { INodeProperties } from "n8n-workflow"; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Me', + value: 'me', + description: 'Get data of authenticated user', + }, + { + name: 'Get', + value: 'get', + description: 'Get data of a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all users', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const userFields = [ + +/* -------------------------------------------------------------------------- */ +/* user:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your users.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'user', + ], + 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: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Is Active', + name: 'is_active', + type: 'string', + default: '', + description: 'Only return users belonging to the user with the given ID.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'string', + default: '', + description: 'Only return users belonging to the user with the given ID.', + }, + { + displayName: 'Page', + name: 'page', + type: 'string', + default: '', + description: 'The page number to use in pagination..', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* user:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Client Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'user', + ], + }, + }, + description: 'The ID of the user you are retrieving.', +} + +] as INodeProperties[]; From a852af653099fbcf6a0fa4b768a5f39faf14ee4b Mon Sep 17 00:00:00 2001 From: trojanh Date: Wed, 29 Jan 2020 12:34:52 +0530 Subject: [PATCH 3/4] Fix UI and API call for each operations --- .../nodes/Harvest/ClientDescription.ts | 2 +- .../nodes/Harvest/CompanyDescription.ts | 2 +- .../nodes/Harvest/ContactDescription.ts | 4 +- .../nodes/Harvest/EstimateDescription.ts | 164 ++++++++++++++++++ .../nodes/Harvest/ExpenseDescription.ts | 6 +- .../nodes-base/nodes/Harvest/Harvest.node.ts | 141 +++++++++++---- .../nodes/Harvest/InvoiceDescription.ts | 4 +- .../nodes/Harvest/ProjectDescription.ts | 4 +- .../nodes/Harvest/TaskDescription.ts | 4 +- .../nodes/Harvest/TimeEntryDescription.ts | 2 +- .../nodes/Harvest/UserDescription.ts | 4 +- 11 files changed, 291 insertions(+), 46 deletions(-) create mode 100644 packages/nodes-base/nodes/Harvest/EstimateDescription.ts diff --git a/packages/nodes-base/nodes/Harvest/ClientDescription.ts b/packages/nodes-base/nodes/Harvest/ClientDescription.ts index 1cefde7da..a8de15c92 100644 --- a/packages/nodes-base/nodes/Harvest/ClientDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ClientDescription.ts @@ -24,7 +24,7 @@ export const clientOperations = [ description: 'Get data of all clients', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, diff --git a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts index 6b85e78bc..26303b7eb 100644 --- a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts +++ b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from "n8n-workflow"; -export const clientOperations = [ +export const companyOperations = [ { displayName: 'Operation', name: 'operation', diff --git a/packages/nodes-base/nodes/Harvest/ContactDescription.ts b/packages/nodes-base/nodes/Harvest/ContactDescription.ts index aa926d233..f4156eeb1 100644 --- a/packages/nodes-base/nodes/Harvest/ContactDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ContactDescription.ts @@ -24,7 +24,7 @@ export const contactOperations = [ description: 'Get data of all contacts', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, @@ -115,7 +115,7 @@ export const contactFields = [ /* contact:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'Contact Id', name: 'id', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Harvest/EstimateDescription.ts b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts new file mode 100644 index 000000000..e16d8ab6c --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts @@ -0,0 +1,164 @@ +import { INodeProperties } from "n8n-workflow"; + +export const estimateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get data of an estimate', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all estimates', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, + +] as INodeProperties[]; + +export const estimateFields = [ + +/* -------------------------------------------------------------------------- */ +/* estimate:getAll */ +/* -------------------------------------------------------------------------- */ + +{ + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your estimates.', +}, +{ + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'estimate', + ], + 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: [ + 'estimate', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + 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: 'State', + name: 'state', + type: 'string', + default: '', + description: 'Only return estimates with a state matching the value provided. Options: draft, sent, accepted, or declined.', + }, + { + 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)', + } + ] +}, + +/* -------------------------------------------------------------------------- */ +/* estimate:get */ +/* -------------------------------------------------------------------------- */ +{ + displayName: 'Estimate Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'estimate', + ], + }, + }, + description: 'The ID of the estimate you are retrieving.', +} + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts index 609a93f6b..831d617bb 100644 --- a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts @@ -16,7 +16,7 @@ export const expenseOperations = [ { name: 'Get', value: 'get', - description: 'Get data of a expense', + description: 'Get data of an expense', }, { name: 'Get All', @@ -24,7 +24,7 @@ export const expenseOperations = [ description: 'Get data of all expenses', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, @@ -157,7 +157,7 @@ export const expenseFields = [ /* expense:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'Expense Id', name: 'id', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Harvest/Harvest.node.ts b/packages/nodes-base/nodes/Harvest/Harvest.node.ts index 4d42ec607..b1e662fc5 100644 --- a/packages/nodes-base/nodes/Harvest/Harvest.node.ts +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -10,6 +10,15 @@ import { import { harvestApiRequest } from './GenericFunctions'; import { timeEntryOperations, timeEntryFields } from './TimeEntryDescription'; +import { clientOperations, clientFields } from './ClientDescription'; +import { companyOperations } from './CompanyDescription'; +import { contactOperations, contactFields } from './ContactDescription'; +import { expenseOperations, expenseFields } from './ExpenseDescription'; +import { invoiceOperations, invoiceFields } from './InvoiceDescription'; +import { projectOperations, projectFields } from './ProjectDescription'; +import { taskOperations, taskFields } from './TaskDescription'; +import { userOperations, userFields } from './UserDescription'; +import { estimateOperations, estimateFields } from './EstimateDescription'; export class Harvest implements INodeType { @@ -47,33 +56,65 @@ export class Harvest implements INodeType { name: "Client", value: "client" }, - { name: "Project", - value: "project"}, - { name: "Contact", - value: "contact"}, - { name: "Company", - value: "company"}, - { name: "Invoice", - value: "invoice"}, - { name: "Task", - value: "task"}, - { name: "User", - value: "user"}, - { name: "Expense", - value: "expense"}, - { name: "Estimates", - value: "estimate"} + { + name: "Project", + value: "project" + }, + { + name: "Contact", + value: "contact" + }, + { + name: "Company", + value: "company" + }, + { + name: "Invoice", + value: "invoice" + }, + { + name: "Task", + value: "task" + }, + { + name: "User", + value: "user" + }, + { + name: "Expense", + value: "expense" + }, + { + name: "Estimates", + value: "estimate" + } ], - default: 'timeEntry', + default: 'user', description: 'The resource to operate on.', }, // operations + ...clientOperations, + ...companyOperations, + ...contactOperations, + ...estimateOperations, + ...expenseOperations, + ...invoiceOperations, + ...projectOperations, + ...taskOperations, ...timeEntryOperations, - ...compa + ...userOperations, // fields - ...timeEntryFields + ...clientFields, + ...contactFields, + ...estimateFields, + ...expenseFields, + ...invoiceFields, + ...projectFields, + ...taskFields, + ...timeEntryFields, + ...userFields ] }; @@ -292,7 +333,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.clients as IDataObject[]); } catch (error) { throw error; } @@ -334,7 +375,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.projects as IDataObject[]); } catch (error) { throw error; } @@ -376,14 +417,14 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.users as IDataObject[]); } catch (error) { throw error; } } else if (operation === 'me') { // ---------------------------------- - // getAll + // me // ---------------------------------- requestMethod = 'GET'; @@ -392,7 +433,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(responseData); + returnData.push(responseData); } catch (error) { throw error; } @@ -434,7 +475,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.contacts as IDataObject[]); } catch (error) { throw error; } @@ -449,8 +490,6 @@ export class Harvest implements INodeType { // ---------------------------------- requestMethod = 'GET'; - const id = this.getNodeParameter('id', i) as string; - endpoint = `company`; try { @@ -497,7 +536,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.companies as IDataObject[]); } catch (error) { throw error; } @@ -539,7 +578,7 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.invoices as IDataObject[]); } catch (error) { throw error; } @@ -581,7 +620,49 @@ export class Harvest implements INodeType { try { let responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - returnData.push.apply(returnData, responseData.time_entries as IDataObject[]); + returnData.push.apply(returnData, responseData.expenses as IDataObject[]); + } catch (error) { + throw error; + } + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'estimate') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + const id = this.getNodeParameter('id', i) as string; + + endpoint = `estimates/${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 = 'estimates'; + + 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.estimates as IDataObject[]); } catch (error) { throw error; } diff --git a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts index edc710afe..d125d22fe 100644 --- a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts +++ b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts @@ -24,7 +24,7 @@ export const invoiceOperations = [ description: 'Get data of all invoices', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, @@ -168,7 +168,7 @@ export const invoiceFields = [ /* invoice:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'Invoice Id', name: 'id', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Harvest/ProjectDescription.ts b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts index 3cbc6536f..e4c43c94e 100644 --- a/packages/nodes-base/nodes/Harvest/ProjectDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ProjectDescription.ts @@ -24,7 +24,7 @@ export const projectOperations = [ description: 'Get data of all projects', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, @@ -130,7 +130,7 @@ export const projectFields = [ /* project:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'Project Id', name: 'id', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Harvest/TaskDescription.ts b/packages/nodes-base/nodes/Harvest/TaskDescription.ts index e1c3020ea..f953a3513 100644 --- a/packages/nodes-base/nodes/Harvest/TaskDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TaskDescription.ts @@ -24,7 +24,7 @@ export const taskOperations = [ description: 'Get data of all tasks', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, @@ -122,7 +122,7 @@ export const taskFields = [ /* task:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'Task Id', name: 'id', type: 'string', default: '', diff --git a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts index 39b3e07ca..121a9defe 100644 --- a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts @@ -59,7 +59,7 @@ export const timeEntryOperations = [ description: 'Update a time entry', }, ], - default: 'create', + default: 'getAll', description: 'The operation to perform.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Harvest/UserDescription.ts b/packages/nodes-base/nodes/Harvest/UserDescription.ts index d78421e09..2a157a47f 100644 --- a/packages/nodes-base/nodes/Harvest/UserDescription.ts +++ b/packages/nodes-base/nodes/Harvest/UserDescription.ts @@ -29,7 +29,7 @@ export const userOperations = [ description: 'Get data of all users', }, ], - default: 'create', + default: 'me', description: 'The operation to perform.', }, @@ -127,7 +127,7 @@ export const userFields = [ /* user:get */ /* -------------------------------------------------------------------------- */ { - displayName: 'Client Id', + displayName: 'User Id', name: 'id', type: 'string', default: '', From e9d8031a68974982d1120cc6ad0a7a2b3808f4b6 Mon Sep 17 00:00:00 2001 From: trojanh Date: Wed, 29 Jan 2020 13:27:54 +0530 Subject: [PATCH 4/4] Add paginations and refactor all getAll methods --- .../nodes/Harvest/GenericFunctions.ts | 17 +- .../nodes-base/nodes/Harvest/Harvest.node.ts | 184 +++++------------- 2 files changed, 54 insertions(+), 147 deletions(-) diff --git a/packages/nodes-base/nodes/Harvest/GenericFunctions.ts b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts index c465197c0..197b18d91 100644 --- a/packages/nodes-base/nodes/Harvest/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts @@ -11,7 +11,7 @@ export async function harvestApiRequest( this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, qs: IDataObject = {}, - uri?: string, + uri: string, body: IDataObject = {}, option: IDataObject = {}, ): Promise { // tslint:disable-line:no-any @@ -45,7 +45,6 @@ export async function harvestApiRequest( "User-Agent": "Harvest API" } }; - console.log({options}) options = Object.assign({}, options, option); if (Object.keys(options.body).length === 0) { @@ -53,7 +52,7 @@ export async function harvestApiRequest( } try { const result = await this.helpers.request!(options); - console.log(result); + return result; } catch (error) { if (error.statusCode === 401) { @@ -79,7 +78,8 @@ export async function harvestApiRequestAllItems( this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, qs: IDataObject = {}, - uri?: string, + uri: string, + resource: string, body: IDataObject = {}, option: IDataObject = {}, ): Promise { // tslint:disable-line:no-any @@ -91,12 +91,9 @@ export async function harvestApiRequestAllItems( 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 - ); + qs.page = responseData.next_page; + returnData.push.apply(returnData, responseData[resource]); + } while (responseData.next_page); 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 index b1e662fc5..3eed31cbb 100644 --- a/packages/nodes-base/nodes/Harvest/Harvest.node.ts +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -8,7 +8,7 @@ import { INodeType, } from 'n8n-workflow'; -import { harvestApiRequest } from './GenericFunctions'; +import { harvestApiRequest, harvestApiRequestAllItems } from './GenericFunctions'; import { timeEntryOperations, timeEntryFields } from './TimeEntryDescription'; import { clientOperations, clientFields } from './ClientDescription'; import { companyOperations } from './CompanyDescription'; @@ -20,6 +20,34 @@ import { taskOperations, taskFields } from './TaskDescription'; import { userOperations, userFields } from './UserDescription'; import { estimateOperations, estimateFields } from './EstimateDescription'; +/** + * fetch All resource using paginated calls + */ +async function getAllResource(this: IExecuteFunctions, resource: string, i: number) { + const endpoint = resource; + let qs: IDataObject = {}; + const requestMethod: string = "GET"; + + qs.per_page = 100; + + const additionalFields = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + Object.assign(qs, additionalFields); + + try { + let responseData: IDataObject = {}; + if(returnAll) { + responseData[resource] = await harvestApiRequestAllItems.call(this, requestMethod, qs, endpoint, resource); + } else { + const limit = this.getNodeParameter('limit', i) as string; + qs.per_page = limit; + responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + } + return responseData[resource] as IDataObject[]; + } catch (error) { + throw error; + } +} export class Harvest implements INodeType { description: INodeTypeDescription = { @@ -158,22 +186,8 @@ export class Harvest implements INodeType { // ---------------------------------- // 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; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'time_entries', i); + returnData.push.apply(returnData, responseData); } else if (operation === 'createByStartEnd') { // ---------------------------------- @@ -322,21 +336,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'clients'; - - 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.clients as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'clients', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -364,21 +365,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'projects'; - - 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.projects as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'projects', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -406,21 +394,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'users'; - - 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.users as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'users', i); + returnData.push.apply(returnData, responseData); } else if (operation === 'me') { // ---------------------------------- @@ -464,21 +439,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'contacts'; - - 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.contacts as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'contacts', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -525,21 +487,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'tasks'; - - 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.companies as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'tasks', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -567,21 +516,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'invoices'; - - 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.invoices as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'invoices', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -609,21 +545,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'expenses'; - - 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.expenses as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'expenses', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`); @@ -651,21 +574,8 @@ export class Harvest implements INodeType { // getAll // ---------------------------------- - requestMethod = 'GET'; - - endpoint = 'estimates'; - - 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.estimates as IDataObject[]); - } catch (error) { - throw error; - } + const responseData: IDataObject[] = await getAllResource.call(this, 'estimates', i); + returnData.push.apply(returnData, responseData); } else { throw new Error(`The resource "${resource}" is not known!`);