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..a8de15c92f --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/ClientDescription.ts @@ -0,0 +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: 'getAll', + 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 0000000000..26303b7eb3 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts @@ -0,0 +1,26 @@ +import { INodeProperties } from "n8n-workflow"; + +export const companyOperations = [ + { + 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 0000000000..f4156eeb1c --- /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: 'getAll', + 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: 'Contact 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/EstimateDescription.ts b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts new file mode 100644 index 0000000000..e16d8ab6c3 --- /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 new file mode 100644 index 0000000000..831d617bbf --- /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 an expense', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get data of all expenses', + }, + ], + default: 'getAll', + 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: 'Expense 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/GenericFunctions.ts b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts new file mode 100644 index 0000000000..197b18d914 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts @@ -0,0 +1,101 @@ +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" + } + }; + + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + const result = await this.helpers.request!(options); + + 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, + resource: 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.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 new file mode 100644 index 0000000000..3eed31cbb3 --- /dev/null +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -0,0 +1,591 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, +} from 'n8n-workflow'; + +import { harvestApiRequest, harvestApiRequestAllItems } 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'; + +/** + * 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 = { + 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', + }, + { + 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: 'user', + description: 'The resource to operate on.', + }, + + // operations + ...clientOperations, + ...companyOperations, + ...contactOperations, + ...estimateOperations, + ...expenseOperations, + ...invoiceOperations, + ...projectOperations, + ...taskOperations, + ...timeEntryOperations, + ...userOperations, + + // fields + ...clientFields, + ...contactFields, + ...estimateFields, + ...expenseFields, + ...invoiceFields, + ...projectFields, + ...taskFields, + ...timeEntryFields, + ...userFields + ] + }; + + 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 + // ---------------------------------- + const responseData: IDataObject[] = await getAllResource.call(this, 'time_entries', i); + returnData.push.apply(returnData, responseData); + + } 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 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'clients', i); + returnData.push.apply(returnData, responseData); + + } 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'projects', i); + returnData.push.apply(returnData, responseData); + + } 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'users', i); + returnData.push.apply(returnData, responseData); + + } else if (operation === 'me') { + // ---------------------------------- + // me + // ---------------------------------- + + requestMethod = 'GET'; + + endpoint = 'users/me'; + + try { + let 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 === '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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'contacts', i); + returnData.push.apply(returnData, responseData); + + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + } else if (resource === 'company') { + if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + requestMethod = 'GET'; + 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'tasks', i); + returnData.push.apply(returnData, responseData); + + } 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'invoices', i); + returnData.push.apply(returnData, responseData); + + } 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'expenses', i); + returnData.push.apply(returnData, responseData); + + } 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 + // ---------------------------------- + + const responseData: IDataObject[] = await getAllResource.call(this, 'estimates', i); + returnData.push.apply(returnData, responseData); + + } else { + throw new Error(`The resource "${resource}" 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/InvoiceDescription.ts b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts new file mode 100644 index 0000000000..d125d22fe4 --- /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: 'getAll', + 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: 'Invoice 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 0000000000..e4c43c94eb --- /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: 'getAll', + 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: 'Project 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 0000000000..f953a35137 --- /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: 'getAll', + 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: 'Task 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/TimeEntryDescription.ts b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts new file mode 100644 index 0000000000..121a9defe2 --- /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: 'getAll', + 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/UserDescription.ts b/packages/nodes-base/nodes/Harvest/UserDescription.ts new file mode 100644 index 0000000000..2a157a47ff --- /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: 'me', + 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: 'User 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[]; diff --git a/packages/nodes-base/nodes/Harvest/harvest.png b/packages/nodes-base/nodes/Harvest/harvest.png new file mode 100644 index 0000000000..5a6713b91d Binary files /dev/null and b/packages/nodes-base/nodes/Harvest/harvest.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d0ff3791e6..7047c1e8a7 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", @@ -135,6 +136,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",