diff --git a/packages/nodes-base/credentials/BeeminderApi.credentials.ts b/packages/nodes-base/credentials/BeeminderApi.credentials.ts new file mode 100644 index 0000000000..d0d126e9e7 --- /dev/null +++ b/packages/nodes-base/credentials/BeeminderApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class BeeminderApi implements ICredentialType { + name = 'beeminderApi'; + displayName = 'Beeminder API'; + properties = [ + { + displayName: 'User', + name: 'user', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth Token', + name: 'authToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Beeminder/Beeminder.node.functions.ts b/packages/nodes-base/nodes/Beeminder/Beeminder.node.functions.ts new file mode 100644 index 0000000000..441b0a1f93 --- /dev/null +++ b/packages/nodes-base/nodes/Beeminder/Beeminder.node.functions.ts @@ -0,0 +1,68 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, +} from 'n8n-workflow'; + +import { + beeminderApiRequest, + beeminderpiRequestAllItems, +} from './GenericFunctions'; + +export async function createDatapoint(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, data: IDataObject) { + const credentials = this.getCredentials('beeminderApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`; + + return await beeminderApiRequest.call(this, 'POST', endpoint, data); +} + +export async function getAllDatapoints(this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions, data: IDataObject) { + const credentials = this.getCredentials('beeminderApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints.json`; + + if (data.count !== undefined) { + return beeminderApiRequest.call(this, 'GET', endpoint, {}, data); + } + + return await beeminderpiRequestAllItems.call(this, 'GET', endpoint, {}, data); +} + +export async function updateDatapoint(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, data: IDataObject) { + const credentials = this.getCredentials('beeminderApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`; + + return await beeminderApiRequest.call(this, 'PUT', endpoint, data); +} + +export async function deleteDatapoint(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, data: IDataObject) { + const credentials = this.getCredentials('beeminderApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `/users/${credentials.user}/goals/${data.goalName}/datapoints/${data.datapointId}.json`; + + return await beeminderApiRequest.call(this, 'DELETE', endpoint); +} + diff --git a/packages/nodes-base/nodes/Beeminder/Beeminder.node.ts b/packages/nodes-base/nodes/Beeminder/Beeminder.node.ts new file mode 100644 index 0000000000..b681bcedd4 --- /dev/null +++ b/packages/nodes-base/nodes/Beeminder/Beeminder.node.ts @@ -0,0 +1,407 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeParameters, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + createDatapoint, + deleteDatapoint, + getAllDatapoints, + updateDatapoint +} from './Beeminder.node.functions'; + +import { + beeminderApiRequest, +} from './GenericFunctions'; + +import * as moment from 'moment-timezone'; + +export class Beeminder implements INodeType { + description: INodeTypeDescription = { + displayName: 'Beeminder', + name: 'beeminder', + group: ['output'], + version: 1, + description: 'Consume Beeminder API', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'Beeminder', + color: '#FFCB06', + }, + icon: 'file:beeminder.png', + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'beeminderApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + required: true, + options: [ + { + name: 'Datapoint', + value: 'datapoint', + }, + ], + default: 'datapoint', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create datapoint for goal.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a datapoint.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all datapoints for a goal.', + }, + { + name: 'Update', + value: 'update', + description: 'Update a datapoint.', + }, + ], + default: 'create', + description: 'The operation to perform.', + required: true, + }, + { + displayName: 'Goal Name', + name: 'goalName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGoals', + }, + displayOptions: { + show: { + resource: [ + 'datapoint', + ], + }, + }, + default: '', + description: 'The name of the goal.', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'datapoint', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'datapoint', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 300, + }, + default: 30, + description: 'How many results to return.', + }, + { + displayName: 'Value', + name: 'value', + type: 'number', + default: 1, + placeholder: '', + description: 'Datapoint value to send.', + displayOptions: { + show: { + resource: [ + 'datapoint', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + }, + { + displayName: 'Datapoint ID', + name: 'datapointId', + type: 'string', + default: '', + description: 'Datapoint id', + displayOptions: { + show: { + operation: [ + 'update', + 'delete', + ], + }, + }, + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'datapoint', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Comment', + name: 'comment', + type: 'string', + default: '', + description: 'Comment', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + placeholder: '', + description: 'Defaults to "now" if none is passed in, or the existing timestamp if the datapoint is being updated rather than created.', + }, + { + displayName: 'Request ID', + name: 'requestId', + type: 'string', + default: '', + placeholder: '', + description: 'String to uniquely identify a datapoint.', + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add field', + default: {}, + displayOptions: { + show: { + resource: [ + 'datapoint', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'string', + default: 'id', + placeholder: '', + description: 'Attribute to sort on.', + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'datapoint', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Value', + name: 'value', + type: 'number', + default: 1, + placeholder: '', + description: 'Datapoint value to send.', + }, + { + displayName: 'Comment', + name: 'comment', + type: 'string', + default: '', + description: 'Comment', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'dateTime', + default: '', + placeholder: '', + description: 'Defaults to "now" if none is passed in, or the existing timestamp if the datapoint is being updated rather than created.', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available groups to display them to user so that he can + // select them easily + async getGoals(this: ILoadOptionsFunctions): Promise { + + const credentials = this.getCredentials('beeminderApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const endpoint = `/users/${credentials.user}/goals.json`; + + const returnData: INodePropertyOptions[] = []; + const goals = await beeminderApiRequest.call(this, 'GET', endpoint); + for (const goal of goals) { + returnData.push({ + name: goal.slug, + value: goal.slug, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const timezone = this.getTimezone(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + let results; + + + for (let i = 0; i < length; i++) { + + if (resource === 'datapoint') { + const goalName = this.getNodeParameter('goalName', i) as string; + if (operation === 'create') { + const value = this.getNodeParameter('value', i) as number; + const options = this.getNodeParameter('additionalFields', i) as INodeParameters; + const data: IDataObject = { + value, + goalName, + }; + Object.assign(data, options); + + if (data.timestamp) { + data.timestamp = moment.tz(data.timestamp, timezone).unix(); + } + console.log(data); + results = await createDatapoint.call(this, data); + } + else if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as INodeParameters; + const data: IDataObject = { + goalName, + }; + Object.assign(data, options); + + if (returnAll === false) { + data.count = this.getNodeParameter('limit', 0) as number; + } + + results = await getAllDatapoints.call(this, data); + } + else if (operation === 'update') { + const datapointId = this.getNodeParameter('datapointId', i) as string; + const options = this.getNodeParameter('updateFields', i) as INodeParameters; + const data: IDataObject = { + goalName, + datapointId, + }; + Object.assign(data, options); + if (data.timestamp) { + data.timestamp = moment.tz(data.timestamp, timezone).unix(); + } + results = await updateDatapoint.call(this, data); + } + else if (operation === 'delete') { + const datapointId = this.getNodeParameter('datapointId', i) as string; + const data: IDataObject = { + goalName, + datapointId, + }; + results = await deleteDatapoint.call(this, data); + } + } + + if (Array.isArray(results)) { + returnData.push.apply(returnData, results as IDataObject[]); + } else { + returnData.push(results as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} + diff --git a/packages/nodes-base/nodes/Beeminder/GenericFunctions.ts b/packages/nodes-base/nodes/Beeminder/GenericFunctions.ts new file mode 100644 index 0000000000..f3541a7c44 --- /dev/null +++ b/packages/nodes-base/nodes/Beeminder/GenericFunctions.ts @@ -0,0 +1,65 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, +} from 'n8n-workflow'; + +const BEEMINDER_URI = 'https://www.beeminder.com/api/v1'; + +export async function beeminderApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('beeminderApi') as IDataObject; + + Object.assign(body, { auth_token: credentials.authToken }); + + const options: OptionsWithUri = { + method, + body, + qs: query, + uri: `${BEEMINDER_URI}${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(query).length) { + delete options.qs; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error?.message) { + throw new Error(`Beeminder error response [${error.statusCode}]: ${error.message}`); + } + throw error; + } +} + +export async function beeminderpiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + do { + responseData = await beeminderApiRequest.call(this, method, endpoint, body, query); + query.page++; + returnData.push.apply(returnData, responseData); + } while ( + responseData.length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Beeminder/beeminder.png b/packages/nodes-base/nodes/Beeminder/beeminder.png new file mode 100644 index 0000000000..ca77fdd340 Binary files /dev/null and b/packages/nodes-base/nodes/Beeminder/beeminder.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6861f35a39..f13409fd86 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -39,6 +39,7 @@ "dist/credentials/Aws.credentials.js", "dist/credentials/AffinityApi.credentials.js", "dist/credentials/BannerbearApi.credentials.js", + "dist/credentials/BeeminderApi.credentials.js", "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/BitlyApi.credentials.js", "dist/credentials/BitlyOAuth2Api.credentials.js", @@ -262,6 +263,7 @@ "dist/nodes/Aws/AwsSns.node.js", "dist/nodes/Aws/AwsSnsTrigger.node.js", "dist/nodes/Bannerbear/Bannerbear.node.js", + "dist/nodes/Beeminder/Beeminder.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js", "dist/nodes/Box/Box.node.js",