feat(Oura Node): Update node for v2 api (#11604)

This commit is contained in:
Jon 2024-11-07 11:53:05 +00:00 committed by GitHub
parent 20fd38f351
commit 3348fbb154
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 299 additions and 80 deletions

View file

@ -1,4 +1,9 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class OuraApi implements ICredentialType {
name = 'ouraApi';
@ -16,4 +21,20 @@ export class OuraApi implements ICredentialType {
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.accessToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.ouraring.com',
url: '/v2/usercollection/personal_info',
},
};
}

View file

@ -4,7 +4,7 @@ import type {
IHookFunctions,
ILoadOptionsFunctions,
JsonObject,
IRequestOptions,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
@ -18,15 +18,11 @@ export async function ouraApiRequest(
uri?: string,
option: IDataObject = {},
) {
const credentials = await this.getCredentials('ouraApi');
let options: IRequestOptions = {
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
},
let options: IHttpRequestOptions = {
method,
qs,
body,
uri: uri || `https://api.ouraring.com/v1${resource}`,
url: uri ?? `https://api.ouraring.com/v2${resource}`,
json: true,
};
@ -41,7 +37,7 @@ export async function ouraApiRequest(
options = Object.assign({}, options, option);
try {
return await this.helpers.request(options);
return await this.helpers.httpRequestWithAuthentication.call(this, 'ouraApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}

View file

@ -63,94 +63,126 @@ export class Oura implements INodeType {
const length = items.length;
let responseData;
const returnData: IDataObject[] = [];
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
if (resource === 'profile') {
// *********************************************************************
// profile
// *********************************************************************
try {
if (resource === 'profile') {
// *********************************************************************
// profile
// *********************************************************************
// https://cloud.ouraring.com/docs/personal-info
// https://cloud.ouraring.com/docs/personal-info
if (operation === 'get') {
// ----------------------------------
// profile: get
// ----------------------------------
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);
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) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
responseData = await ouraApiRequest.call(this, 'GET', '/usercollection/personal_info');
}
} else if (operation === 'getReadiness') {
// ----------------------------------
// profile: getReadiness
// ----------------------------------
} else if (resource === 'summary') {
// *********************************************************************
// summary
// *********************************************************************
responseData = await ouraApiRequest.call(this, 'GET', '/readiness', {}, qs);
responseData = responseData.readiness;
// https://cloud.ouraring.com/docs/daily-summaries
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
const qs: IDataObject = {};
const { start, end } = this.getNodeParameter('filters', i) as {
start: string;
end: string;
};
const returnAll = this.getNodeParameter('returnAll', 0);
if (start) {
qs.start_date = moment(start).format('YYYY-MM-DD');
}
} else if (operation === 'getSleep') {
// ----------------------------------
// profile: getSleep
// ----------------------------------
responseData = await ouraApiRequest.call(this, 'GET', '/sleep', {}, qs);
responseData = responseData.sleep;
if (end) {
qs.end_date = moment(end).format('YYYY-MM-DD');
}
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
if (operation === 'getActivity') {
// ----------------------------------
// profile: getActivity
// ----------------------------------
responseData = await ouraApiRequest.call(
this,
'GET',
'/usercollection/daily_activity',
{},
qs,
);
responseData = responseData.data;
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
}
} else if (operation === 'getReadiness') {
// ----------------------------------
// profile: getReadiness
// ----------------------------------
responseData = await ouraApiRequest.call(
this,
'GET',
'/usercollection/daily_readiness',
{},
qs,
);
responseData = responseData.data;
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
}
} else if (operation === 'getSleep') {
// ----------------------------------
// profile: getSleep
// ----------------------------------
responseData = await ouraApiRequest.call(
this,
'GET',
'/usercollection/daily_sleep',
{},
qs,
);
responseData = responseData.data;
if (!returnAll) {
const limit = this.getNodeParameter('limit', 0);
responseData = responseData.splice(0, limit);
}
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
Array.isArray(responseData)
? returnData.push(...(responseData as IDataObject[]))
: returnData.push(responseData as IDataObject);
}
return [this.helpers.returnJsonArray(returnData)];
return [returnData];
}
}

View file

@ -0,0 +1,8 @@
export const profileResponse = {
id: 'some-id',
age: 30,
weight: 168,
height: 80,
biological_sex: 'male',
email: 'nathan@n8n.io',
};

View file

@ -0,0 +1,76 @@
import type {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IHttpRequestMethods,
INode,
} from 'n8n-workflow';
import nock from 'nock';
import { setup, equalityTest, workflowToTests, getWorkflowFilenames } from '@test/nodes/Helpers';
import { profileResponse } from './apiResponses';
import { ouraApiRequest } from '../GenericFunctions';
const node: INode = {
id: '2cdb46cf-b561-4537-a982-b8d26dd7718b',
name: 'Oura',
type: 'n8n-nodes-base.oura',
typeVersion: 1,
position: [0, 0],
parameters: {
resource: 'profile',
operation: 'get',
},
};
const mockThis = {
helpers: {
httpRequestWithAuthentication: jest
.fn()
.mockResolvedValue({ statusCode: 200, data: profileResponse }),
},
getNode() {
return node;
},
getNodeParameter: jest.fn(),
} as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions;
describe('Oura', () => {
describe('ouraApiRequest', () => {
it('should make an authenticated API request to Oura', async () => {
const method: IHttpRequestMethods = 'GET';
const resource = '/usercollection/personal_info';
await ouraApiRequest.call(mockThis, method, resource);
expect(mockThis.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('ouraApi', {
method: 'GET',
url: 'https://api.ouraring.com/v2/usercollection/personal_info',
json: true,
});
});
});
describe('Run Oura workflow', () => {
const workflows = getWorkflowFilenames(__dirname);
const tests = workflowToTests(workflows);
beforeAll(() => {
nock.disableNetConnect();
nock('https://api.ouraring.com/v2')
.get('/usercollection/personal_info')
.reply(200, profileResponse);
});
afterAll(() => {
nock.restore();
});
const nodeTypes = setup(tests);
for (const testData of tests) {
test(testData.description, async () => await equalityTest(testData, nodeTypes));
}
});
});

View file

@ -0,0 +1,86 @@
{
"name": "Oura Test Workflow",
"nodes": [
{
"parameters": {},
"id": "c1e3b825-a9a8-4def-986b-9108d9441992",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [720, 400],
"typeVersion": 1
},
{
"parameters": {
"resource": "profile"
},
"id": "7969bf78-9343-4f81-8f79-dc415a60e168",
"name": "Oura",
"type": "n8n-nodes-base.oura",
"typeVersion": 1,
"position": [940, 400],
"credentials": {
"ouraApi": {
"id": "r083EOdhFatkVvFy",
"name": "Oura account"
}
}
},
{
"parameters": {},
"id": "9b97fa0e-51a6-41d3-8a7d-cff0531e5527",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1140, 400]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"id": "some-id",
"age": 30,
"weight": 168,
"height": 80,
"biological_sex": "male",
"email": "nathan@n8n.io"
}
}
]
},
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Oura",
"type": "main",
"index": 0
}
]
]
},
"Oura": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "bd108f46-f6fc-4c22-8655-ade2f51c4b33",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d"
},
"id": "SrUileWU90mQeo02",
"tags": []
}