From 95b33662c9e8404618b802a93520fe00b14de5cb Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 22 Oct 2020 04:17:39 -0400 Subject: [PATCH] :sparkles: Strava Node & Trigger (#1071) * :sparkles: Strava Node & Trigger * :zap: Small fixes * :zap: Add improvements --- .../StravaOAuth2Api.credentials.ts | 47 ++ .../nodes/Strava/ActivityDescription.ts | 412 ++++++++++++++++++ .../nodes/Strava/GenericFunctions.ts | 92 ++++ .../nodes-base/nodes/Strava/Strava.node.ts | 175 ++++++++ .../nodes/Strava/StravaTrigger.node.ts | 273 ++++++++++++ packages/nodes-base/nodes/Strava/strava.svg | 1 + packages/nodes-base/package.json | 3 + 7 files changed, 1003 insertions(+) create mode 100644 packages/nodes-base/credentials/StravaOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Strava/ActivityDescription.ts create mode 100644 packages/nodes-base/nodes/Strava/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Strava/Strava.node.ts create mode 100644 packages/nodes-base/nodes/Strava/StravaTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Strava/strava.svg diff --git a/packages/nodes-base/credentials/StravaOAuth2Api.credentials.ts b/packages/nodes-base/credentials/StravaOAuth2Api.credentials.ts new file mode 100644 index 0000000000..743f577004 --- /dev/null +++ b/packages/nodes-base/credentials/StravaOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class StravaOAuth2Api implements ICredentialType { + name = 'stravaOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Strava OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.strava.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.strava.com/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'activity:read_all,activity:write', + required: true + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Strava/ActivityDescription.ts b/packages/nodes-base/nodes/Strava/ActivityDescription.ts new file mode 100644 index 0000000000..43248d70e8 --- /dev/null +++ b/packages/nodes-base/nodes/Strava/ActivityDescription.ts @@ -0,0 +1,412 @@ + +import { + INodeProperties, +} from "n8n-workflow"; + +export const activityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'activity', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new activity', + }, + { + name: 'Get', + value: 'get', + description: 'Get an activity', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all activities', + }, + { + name: 'Get Comments', + value: 'getComments', + description: 'Get all activity comments', + }, + { + name: 'Get Kudoers', + value: 'getKudoers', + description: 'Get all activity kudoers', + }, + { + name: 'Get Laps', + value: 'getLaps', + description: 'Get all activity laps', + }, + { + name: 'Get Zones', + value: 'getZones', + description: 'Get all activity zones', + }, + { + name: 'Update', + value: 'update', + description: 'Update an activity', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const activityFields = [ + +/* -------------------------------------------------------------------------- */ +/* activity:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create' + ] + }, + }, + default: '', + description: 'The name of the activity', + }, + { + displayName: 'Type', + name: 'type', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create' + ] + }, + }, + default: '', + description: 'Type of activity. For example - Run, Ride etc.', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create' + ] + }, + }, + description: 'ISO 8601 formatted date time.', + }, + { + displayName: 'Elapsed Time (Seconds)', + name: 'elapsedTime', + type: 'number', + required :true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create' + ] + }, + }, + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'In seconds.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Commute', + name: 'commute', + type: 'boolean', + default: false, + description: 'Set to true to mark as commute.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the activity.', + }, + { + displayName: 'Distance', + name: 'distance', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'In meters.', + }, + { + displayName: 'Trainer', + name: 'trainer', + type: 'boolean', + default: false, + description: 'Set to true to mark as a trainer activity.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* activity:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Activity ID', + name: 'activityId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID or email of activity', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Commute', + name: 'commute', + type: 'boolean', + default: false, + description: 'Set to true to mark as commute.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'Description of the activity.', + }, + { + displayName: 'Gear ID', + name: 'gear_id', + type: 'string', + default: '', + description: 'Identifier for the gear associated with the activity. ‘none’ clears gear from activity', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the activity', + }, + { + displayName: 'Type', + name: 'type', + type: 'string', + default: '', + description: 'Type of activity. For example - Run, Ride etc.', + }, + { + displayName: 'Trainer', + name: 'trainer', + type: 'boolean', + default: false, + description: 'Set to true to mark as a trainer activity.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* activity:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Activity ID', + name: 'activityId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'ID or email of activity', + }, +/* -------------------------------------------------------------------------- */ +/* activity */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Activity ID', + name: 'activityId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'comment', + 'lap', + 'kudo', + 'zone', + ], + }, + }, + default: '', + description: 'ID or email of activity', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getComments', + 'getLaps', + 'getKudoers', + 'getZones', + ], + }, + }, + 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: [ + 'activity', + ], + operation: [ + 'getComments', + 'getLaps', + 'getKudoers', + 'getZones', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +/* -------------------------------------------------------------------------- */ +/* activity:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + 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: [ + 'activity', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Strava/GenericFunctions.ts b/packages/nodes-base/nodes/Strava/GenericFunctions.ts new file mode 100644 index 0000000000..5de75cd91d --- /dev/null +++ b/packages/nodes-base/nodes/Strava/GenericFunctions.ts @@ -0,0 +1,92 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function stravaApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + form: body, + qs, + uri: uri || `https://www.strava.com/api/v3${resource}`, + json: true + }; + try { + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (this.getNode().type.includes('Trigger') && resource.includes('/push_subscriptions')) { + const credentials = this.getCredentials('stravaOAuth2Api') as IDataObject; + if (method === 'GET') { + qs.client_id = credentials.clientId; + qs.client_secret = credentials.clientSecret; + } else { + body.client_id = credentials.clientId; + body.client_secret = credentials.clientSecret; + } + //@ts-ignore + return this.helpers?.request(options); + + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'stravaOAuth2Api', options); + } + } catch (error) { + + if (error.statusCode === 402) { + throw new Error( + `Strava error response [${error.statusCode}]: Payment Required` + ); + } + + if (error.response && error.response.body && error.response.body.errors) { + + let errors = error.response.body.errors; + + errors = errors.map((e: IDataObject) => `${e.code} -> ${e.field}`); + // Try to return the error prettier + throw new Error( + `Strava error response [${error.statusCode}]: ${errors.join('|')}` + ); + } + throw error; + } +} + +export async function stravaApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.per_page = 30; + + query.page = 1; + + do { + responseData = await stravaApiRequest.call(this, method, resource, body, query); + query.page++; + returnData.push.apply(returnData, responseData); + } while ( + responseData.length !== 0 + ); + + return returnData; +} + diff --git a/packages/nodes-base/nodes/Strava/Strava.node.ts b/packages/nodes-base/nodes/Strava/Strava.node.ts new file mode 100644 index 0000000000..578fd65fdc --- /dev/null +++ b/packages/nodes-base/nodes/Strava/Strava.node.ts @@ -0,0 +1,175 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + stravaApiRequest, + stravaApiRequestAllItems, +} from './GenericFunctions'; + +import { + activityFields, + activityOperations, +} from './ActivityDescription'; + +import * as moment from 'moment'; + +export class Strava implements INodeType { + description: INodeTypeDescription = { + displayName: 'Strava', + name: 'strava', + icon: 'file:strava.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Strava API.', + defaults: { + name: 'Strava', + color: '#ea5929', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'stravaOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Activity', + value: 'activity', + }, + ], + default: 'activity', + description: 'The resource to operate on.' + }, + ...activityOperations, + ...activityFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + 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 === 'activity') { + //https://developers.strava.com/docs/reference/#api-Activities-createActivity + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + + const type = this.getNodeParameter('type', i) as string; + + const startDate = this.getNodeParameter('startDate', i) as string; + + const elapsedTime = this.getNodeParameter('elapsedTime', i) as number; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.trainer === true) { + additionalFields.trainer = 1; + } + + if (additionalFields.commute === true) { + additionalFields.commute = 1; + } + + const body: IDataObject = { + name, + type, + start_date_local: moment(startDate).toISOString(), + elapsed_time: elapsedTime, + }; + + Object.assign(body, additionalFields); + + responseData = await stravaApiRequest.call(this, 'POST', '/activities', body); + } + //https://developers.strava.com/docs/reference/#api-Activities-getActivityById + if (operation === 'get') { + const activityId = this.getNodeParameter('activityId', i) as string; + + responseData = await stravaApiRequest.call(this, 'GET', `/activities/${activityId}`); + } + if (['getLaps', 'getZones', 'getKudoers', 'getComments'].includes(operation)) { + + const path: IDataObject = { + 'getComments': 'comments', + 'getZones': 'zones', + 'getKudoers': 'kudoers', + 'getLaps': 'laps', + }; + + const activityId = this.getNodeParameter('activityId', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await stravaApiRequest.call(this, 'GET', `/activities/${activityId}/${path[operation]}`); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://developers.mailerlite.com/reference#subscribers + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll) { + + responseData = await stravaApiRequestAllItems.call(this, 'GET', `/activities`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + + responseData = await stravaApiRequest.call(this, 'GET', `/activities`, {}, qs); + } + } + //https://developers.strava.com/docs/reference/#api-Activities-updateActivityById + if (operation === 'update') { + const activityId = this.getNodeParameter('activityId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (updateFields.trainer === true) { + updateFields.trainer = 1; + } + + if (updateFields.commute === true) { + updateFields.commute = 1; + } + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + responseData = await stravaApiRequest.call(this, 'PUT', `/activities/${activityId}`, body); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts b/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts new file mode 100644 index 0000000000..7b3bea5c6e --- /dev/null +++ b/packages/nodes-base/nodes/Strava/StravaTrigger.node.ts @@ -0,0 +1,273 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + stravaApiRequest, +} from './GenericFunctions'; + +import { + randomBytes, +} from 'crypto'; + +export class StravaTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Strava Trigger', + name: 'stravTrigger', + icon: 'file:strava.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a Github events occurs.', + defaults: { + name: 'Strava Trigger', + color: '#ea5929', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'stravaOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'GET', + responseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Object', + name: 'object', + type: 'options', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Athlete', + value: 'athlete', + }, + ], + default: '*', + }, + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: '*', + value: '*', + }, + { + name: 'created', + value: 'create', + }, + { + name: 'Deleted', + value: 'delete', + }, + { + name: 'Updated', + value: 'update', + }, + ], + default: '*', + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: true, + description: 'By default the webhook-data only contain the Object ID. If this option gets activated it
will resolve the data automatically.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Delete If Exist', + name: 'deleteIfExist', + type: 'boolean', + default: false, + description: `Strava allows just one subscription at all times. If you want to delete the current subscription to make
+ room for a new subcription with the current parameters, set this parameter to true. Keep in mind this is a destructive operation.`, + }, + ], + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/push_subscriptions'; + + const webhooks = await stravaApiRequest.call(this, 'GET', endpoint, {}); + + for (const webhook of webhooks) { + if (webhook.callback_url === webhookUrl) { + webhookData.webhookId = webhook.id; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + + const endpoint = '/push_subscriptions'; + + const body = { + callback_url: webhookUrl, + verify_token: randomBytes(20).toString('hex') as string, + }; + + let responseData; + + try { + responseData = await stravaApiRequest.call(this, 'POST', endpoint, body); + } catch (error) { + const errors = error.response.body.errors; + for (error of errors) { + // if there is a subscription already created + if (error.resource === 'PushSubscription' && error.code === 'already exists') { + const options = this.getNodeParameter('options') as IDataObject; + //get the current subscription + const webhooks = await stravaApiRequest.call(this, 'GET', `/push_subscriptions`, {}); + + if (options.deleteIfExist) { + // delete the subscription + await stravaApiRequest.call(this, 'DELETE', `/push_subscriptions/${webhooks[0].id}`); + // now there is room create a subscription with the n8n data + const body = { + callback_url: webhookUrl, + verify_token: randomBytes(20).toString('hex') as string, + }; + + responseData = await stravaApiRequest.call(this, 'POST', `/push_subscriptions`, body); + } else { + throw new Error(`A subscription already exist [${webhooks[0].callback_url}]. + If you want to delete this subcription and create a new one with the current parameters please go to options and set delete if exist to true`); + } + } + } + } + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/push_subscriptions/${webhookData.webhookId}`; + + try { + await stravaApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const body = this.getBodyData() as IDataObject; + const query = this.getQueryData() as IDataObject; + const object = this.getNodeParameter('object'); + const event = this.getNodeParameter('event'); + const resolveData = this.getNodeParameter('resolveData') as boolean; + + let objectType, eventType; + + if (object === '*') { + objectType = ['activity', 'athlete']; + } else { + objectType = [object]; + } + + if (event === '*') { + eventType = ['create', 'update', 'delete']; + } else { + eventType = [event]; + } + + if (this.getWebhookName() === 'setup') { + if (query['hub.challenge']) { + // Is a create webhook confirmation request + const res = this.getResponseObject(); + res.status(200).json({ 'hub.challenge': query['hub.challenge'] }).end(); + return { + noWebhookResponse: true, + }; + } + } + + if (object !== '*' && !objectType.includes(body.object_type as string)) { + return {}; + } + + if (event !== '*' && !eventType.includes(body.aspect_type as string)) { + return {}; + } + + if (resolveData) { + let endpoint = `/athletes/${body.object_id}/stats`; + if (body.object_type === 'activity') { + endpoint = `/activities/${body.object_id}`; + } + body.object_data = await stravaApiRequest.call(this, 'GET', endpoint); + } + + return { + workflowData: [ + this.helpers.returnJsonArray(body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Strava/strava.svg b/packages/nodes-base/nodes/Strava/strava.svg new file mode 100644 index 0000000000..d94ac6a66f --- /dev/null +++ b/packages/nodes-base/nodes/Strava/strava.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index da9653bb3d..8f0461ceca 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -155,6 +155,7 @@ "dist/credentials/SlackOAuth2Api.credentials.js", "dist/credentials/Sms77Api.credentials.js", "dist/credentials/Smtp.credentials.js", + "dist/credentials/StravaOAuth2Api.credentials.js", "dist/credentials/StripeApi.credentials.js", "dist/credentials/SalesmateApi.credentials.js", "dist/credentials/SegmentApi.credentials.js", @@ -357,6 +358,8 @@ "dist/nodes/SpreadsheetFile.node.js", "dist/nodes/SseTrigger.node.js", "dist/nodes/Start.node.js", + "dist/nodes/Strava/Strava.node.js", + "dist/nodes/Strava/StravaTrigger.node.js", "dist/nodes/Stripe/StripeTrigger.node.js", "dist/nodes/Switch.node.js", "dist/nodes/Salesmate/Salesmate.node.js",