From 45c0d6598f5b71094b1228b83f3137b8221d397b Mon Sep 17 00:00:00 2001 From: Tanay Pant <7481165+tanay1337@users.noreply.github.com> Date: Fri, 2 Apr 2021 18:29:20 +0200 Subject: [PATCH] :sparkles: Add Oura node (#1609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add Oura node * :hammer: Make it work * :zap: Improvements * :zap: Improvements * :art: Fix SVG size and position * :zap: Fix parameter error & other improvements Co-authored-by: Iván Ovejero Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/OuraApi.credentials.ts | 18 ++ .../nodes-base/nodes/Oura/GenericFunctions.ts | 63 +++++++ packages/nodes-base/nodes/Oura/Oura.node.ts | 178 ++++++++++++++++++ .../nodes/Oura/ProfileDescription.ts | 27 +++ .../nodes/Oura/SummaryDescription.ts | 105 +++++++++++ packages/nodes-base/nodes/Oura/oura.svg | 16 ++ packages/nodes-base/package.json | 2 + 7 files changed, 409 insertions(+) create mode 100644 packages/nodes-base/credentials/OuraApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Oura/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Oura/Oura.node.ts create mode 100644 packages/nodes-base/nodes/Oura/ProfileDescription.ts create mode 100644 packages/nodes-base/nodes/Oura/SummaryDescription.ts create mode 100644 packages/nodes-base/nodes/Oura/oura.svg diff --git a/packages/nodes-base/credentials/OuraApi.credentials.ts b/packages/nodes-base/credentials/OuraApi.credentials.ts new file mode 100644 index 0000000000..1f1ec00e3d --- /dev/null +++ b/packages/nodes-base/credentials/OuraApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class OuraApi implements ICredentialType { + name = 'ouraApi'; + displayName = 'Oura API'; + documentationUrl = 'oura'; + properties = [ + { + displayName: 'Personal Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Oura/GenericFunctions.ts b/packages/nodes-base/nodes/Oura/GenericFunctions.ts new file mode 100644 index 0000000000..cd52b4bb4d --- /dev/null +++ b/packages/nodes-base/nodes/Oura/GenericFunctions.ts @@ -0,0 +1,63 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function ouraApiRequest( + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + option: IDataObject = {}, +) { + + const credentials = this.getCredentials('ouraApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + method, + qs, + body, + uri: uri || `https://api.ouraring.com/v1${resource}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + options = Object.assign({}, options, option); + + try { + return await this.helpers.request!(options); + } catch (error) { + + const errorMessage = error?.response?.body?.message; + + if (errorMessage) { + throw new Error(`Oura error response [${error.statusCode}]: ${errorMessage}`); + } + + throw error; + } +} diff --git a/packages/nodes-base/nodes/Oura/Oura.node.ts b/packages/nodes-base/nodes/Oura/Oura.node.ts new file mode 100644 index 0000000000..58dfeb8890 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/Oura.node.ts @@ -0,0 +1,178 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + ouraApiRequest, +} from './GenericFunctions'; + +import { + profileOperations, +} from './ProfileDescription'; + +import { + summaryFields, + summaryOperations, +} from './SummaryDescription'; + +import * as moment from 'moment'; + +export class Oura implements INodeType { + description: INodeTypeDescription = { + displayName: 'Oura', + name: 'oura', + icon: 'file:oura.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Oura API', + defaults: { + name: 'Oura', + color: '#2f4a73', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'ouraApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Profile', + value: 'profile', + }, + { + name: 'Summary', + value: 'summary', + }, + ], + default: 'summary', + description: 'Resource to consume.', + }, + ...profileOperations, + ...summaryOperations, + ...summaryFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length; + + let responseData; + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + + if (resource === 'profile') { + + // ********************************************************************* + // profile + // ********************************************************************* + + // https://cloud.ouraring.com/docs/personal-info + + if (operation === 'get') { + + // ---------------------------------- + // profile: get + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/userinfo'); + + } + + } else if (resource === 'summary') { + + // ********************************************************************* + // summary + // ********************************************************************* + + // https://cloud.ouraring.com/docs/daily-summaries + + const qs: IDataObject = {}; + + const { start, end } = this.getNodeParameter('filters', i) as { start: string; end: string; }; + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (start) { + qs.start = moment(start).format('YYYY-MM-DD'); + } + + if (end) { + qs.end = moment(end).format('YYYY-MM-DD'); + } + + if (operation === 'getActivity') { + + // ---------------------------------- + // profile: getActivity + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/activity', {}, qs); + responseData = responseData.activity; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } else if (operation === 'getReadiness') { + + // ---------------------------------- + // profile: getReadiness + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/readiness', {}, qs); + responseData = responseData.readiness; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } else if (operation === 'getSleep') { + + // ---------------------------------- + // profile: getSleep + // ---------------------------------- + + responseData = await ouraApiRequest.call(this, 'GET', '/sleep', {}, qs); + responseData = responseData.sleep; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Oura/ProfileDescription.ts b/packages/nodes-base/nodes/Oura/ProfileDescription.ts new file mode 100644 index 0000000000..b3e028c8a8 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/ProfileDescription.ts @@ -0,0 +1,27 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get the user\'s personal information.', + }, + ], + default: 'get', + description: 'Operation to perform.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Oura/SummaryDescription.ts b/packages/nodes-base/nodes/Oura/SummaryDescription.ts new file mode 100644 index 0000000000..0512eb1e22 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/SummaryDescription.ts @@ -0,0 +1,105 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const summaryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + options: [ + { + name: 'Get Activity Summary', + value: 'getActivity', + description: 'Get the user\'s activity summary.', + }, + { + name: 'Get Readiness Summary', + value: 'getReadiness', + description: 'Get the user\'s readiness summary.', + }, + { + name: 'Get Sleep Periods', + value: 'getSleep', + description: 'Get the user\'s sleep summary.', + }, + ], + default: 'getSleep', + description: 'Operation to perform.', + }, +] as INodeProperties[]; + +export const summaryFields = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'summary', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'summary', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'End Date', + name: 'end', + type: 'dateTime', + default: '', + description: 'End date for the summary retrieval. If omitted, it defaults to the current day.', + }, + { + displayName: 'Start Date', + name: 'start', + type: 'dateTime', + default: '', + description: 'Start date for the summary retrieval. If omitted, it defaults to a week ago.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Oura/oura.svg b/packages/nodes-base/nodes/Oura/oura.svg new file mode 100644 index 0000000000..4d4a1adc66 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/oura.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ffeea6b355..556d7cfc1b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -169,6 +169,7 @@ "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", "dist/credentials/OrbitApi.credentials.js", + "dist/credentials/OuraApi.credentials.js", "dist/credentials/PaddleApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", "dist/credentials/PagerDutyOAuth2Api.credentials.js", @@ -436,6 +437,7 @@ "dist/nodes/OpenThesaurus/OpenThesaurus.node.js", "dist/nodes/OpenWeatherMap.node.js", "dist/nodes/Orbit/Orbit.node.js", + "dist/nodes/Oura/Oura.node.js", "dist/nodes/Paddle/Paddle.node.js", "dist/nodes/PagerDuty/PagerDuty.node.js", "dist/nodes/PayPal/PayPal.node.js",