first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,44 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
JsonObject,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function ouraApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
uri?: string,
option: IDataObject = {},
) {
let options: IHttpRequestOptions = {
method,
qs,
body,
url: uri ?? `https://api.ouraring.com/v2${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.httpRequestWithAuthentication.call(this, 'ouraApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.oura",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/oura/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.oura/"
}
]
}
}
+187
View File
@@ -0,0 +1,187 @@
import moment from 'moment-timezone';
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { ouraApiRequest } from './GenericFunctions';
import { profileOperations } from './ProfileDescription';
import { summaryFields, summaryOperations } from './SummaryDescription';
export class Oura implements INodeType {
description: INodeTypeDescription = {
displayName: 'Oura',
name: 'oura',
icon: { light: 'file:oura.svg', dark: 'file:oura.dark.svg' },
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Oura API',
defaults: {
name: 'Oura',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'ouraApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Profile',
value: 'profile',
},
{
name: 'Summary',
value: 'summary',
},
],
default: 'summary',
},
...profileOperations,
...summaryOperations,
...summaryFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = items.length;
let responseData;
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
try {
if (resource === 'profile') {
// *********************************************************************
// profile
// *********************************************************************
// https://cloud.ouraring.com/docs/personal-info
if (operation === 'get') {
// ----------------------------------
// profile: get
// ----------------------------------
responseData = await ouraApiRequest.call(this, 'GET', '/usercollection/personal_info');
}
} 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_date = moment(start).format('YYYY-MM-DD');
}
if (end) {
qs.end_date = moment(end).format('YYYY-MM-DD');
}
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;
}
}
return [returnData];
}
}
@@ -0,0 +1,24 @@
import type { INodeProperties } from 'n8n-workflow';
export const profileOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['profile'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: "Get the user's personal information",
action: 'Get a profile',
},
],
default: 'get',
},
];
@@ -0,0 +1,97 @@
import type { INodeProperties } from 'n8n-workflow';
export const summaryOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['summary'],
},
},
options: [
{
name: 'Get Activity Summary',
value: 'getActivity',
description: "Get the user's activity summary",
action: 'Get activity summary',
},
{
name: 'Get Readiness Summary',
value: 'getReadiness',
description: "Get the user's readiness summary",
action: 'Get readiness summary',
},
{
name: 'Get Sleep Periods',
value: 'getSleep',
description: "Get the user's sleep summary",
action: 'Get sleep summary',
},
],
default: 'getSleep',
},
];
export const summaryFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['summary'],
},
},
default: false,
description: 'Whether to return all results 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: 'Max number of 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.',
},
],
},
];
@@ -0,0 +1,24 @@
{
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"biological_sex": {
"type": "string"
},
"email": {
"type": "string"
},
"height": {
"type": "number"
},
"id": {
"type": "string"
},
"weight": {
"type": "number"
}
},
"version": 1
}
@@ -0,0 +1,118 @@
{
"type": "object",
"properties": {
"active_calories": {
"type": "integer"
},
"average_met_minutes": {
"type": "number"
},
"class_5_min": {
"type": "string"
},
"contributors": {
"type": "object",
"properties": {
"meet_daily_targets": {
"type": "integer"
},
"move_every_hour": {
"type": "integer"
},
"recovery_time": {
"type": "integer"
},
"stay_active": {
"type": "integer"
},
"training_frequency": {
"type": "integer"
},
"training_volume": {
"type": "integer"
}
}
},
"day": {
"type": "string"
},
"equivalent_walking_distance": {
"type": "integer"
},
"high_activity_met_minutes": {
"type": "integer"
},
"high_activity_time": {
"type": "integer"
},
"id": {
"type": "string"
},
"inactivity_alerts": {
"type": "integer"
},
"low_activity_met_minutes": {
"type": "integer"
},
"low_activity_time": {
"type": "integer"
},
"medium_activity_met_minutes": {
"type": "integer"
},
"medium_activity_time": {
"type": "integer"
},
"met": {
"type": "object",
"properties": {
"interval": {
"type": "integer"
},
"items": {
"type": "array",
"items": {
"type": "number"
}
},
"timestamp": {
"type": "string"
}
}
},
"meters_to_target": {
"type": "integer"
},
"non_wear_time": {
"type": "integer"
},
"resting_time": {
"type": "integer"
},
"score": {
"type": "integer"
},
"sedentary_met_minutes": {
"type": "integer"
},
"sedentary_time": {
"type": "integer"
},
"steps": {
"type": "integer"
},
"target_calories": {
"type": "integer"
},
"target_meters": {
"type": "integer"
},
"timestamp": {
"type": "string"
},
"total_calories": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,41 @@
{
"type": "object",
"properties": {
"contributors": {
"type": "object",
"properties": {
"activity_balance": {
"type": "integer"
},
"body_temperature": {
"type": "integer"
},
"previous_night": {
"type": "integer"
},
"recovery_index": {
"type": "integer"
},
"resting_heart_rate": {
"type": "integer"
}
}
},
"day": {
"type": "string"
},
"id": {
"type": "string"
},
"score": {
"type": "integer"
},
"temperature_deviation": {
"type": "number"
},
"timestamp": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,44 @@
{
"type": "object",
"properties": {
"contributors": {
"type": "object",
"properties": {
"deep_sleep": {
"type": "integer"
},
"efficiency": {
"type": "integer"
},
"latency": {
"type": "integer"
},
"rem_sleep": {
"type": "integer"
},
"restfulness": {
"type": "integer"
},
"timing": {
"type": "integer"
},
"total_sleep": {
"type": "integer"
}
}
},
"day": {
"type": "string"
},
"id": {
"type": "string"
},
"score": {
"type": "integer"
},
"timestamp": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2219 3.54937H28.6724V0H12.2219V3.54937ZM20.4505 7.09861C11.3797 7.09861 4 14.4783 4 23.5491C4 32.6203 11.3797 40 20.4505 40C29.5217 40 36.9014 32.6203 36.9014 23.5491C36.9014 14.4783 29.5217 7.09861 20.4505 7.09861ZM20.4505 36.4508C13.3366 36.4508 7.54885 30.663 7.54885 23.5491C7.54885 16.4353 13.3367 10.6478 20.4506 10.6478C27.5647 10.6478 33.3527 16.4353 33.3527 23.5491C33.3527 30.663 27.5647 36.4508 20.4506 36.4508" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2219 3.54937H28.6724V0H12.2219V3.54937ZM20.4505 7.09861C11.3797 7.09861 4 14.4783 4 23.5491C4 32.6203 11.3797 40 20.4505 40C29.5217 40 36.9014 32.6203 36.9014 23.5491C36.9014 14.4783 29.5217 7.09861 20.4505 7.09861ZM20.4505 36.4508C13.3366 36.4508 7.54885 30.663 7.54885 23.5491C7.54885 16.4353 13.3367 10.6478 20.4506 10.6478C27.5647 10.6478 33.3527 16.4353 33.3527 23.5491C33.3527 30.663 27.5647 36.4508 20.4506 36.4508" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1,8 @@
export const profileResponse = {
id: 'some-id',
age: 30,
weight: 168,
height: 80,
biological_sex: 'male',
email: 'nathan@n8n.io',
};
@@ -0,0 +1,63 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import type {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IHttpRequestMethods,
INode,
} from 'n8n-workflow';
import nock from 'nock';
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', () => {
beforeAll(() => {
nock('https://api.ouraring.com/v2')
.get('/usercollection/personal_info')
.reply(200, profileResponse);
});
new NodeTestHarness().setupTests();
});
});
@@ -0,0 +1,86 @@
{
"name": "Oura Test Workflow",
"nodes": [
{
"parameters": {},
"id": "c1e3b825-a9a8-4def-986b-9108d9441992",
"name": "When clicking Execute 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 Execute 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": []
}