From af09c5654bea052614437662cc3e3216aa87033e Mon Sep 17 00:00:00 2001 From: Rafael Franco Date: Mon, 30 Nov 2020 20:05:15 -0300 Subject: [PATCH] feat: add activity methods --- package-lock.json | 23 ++++ package.json | 8 +- src/enums/activityType.ts | 39 +++++++ src/enums/activityZoneType.ts | 4 + src/enums/followerStatus.ts | 5 + src/enums/index.ts | 8 ++ src/enums/resourceState.ts | 5 + src/enums/sex.ts | 4 + src/enums/sportType.ts | 6 ++ src/enums/streamKeys.ts | 13 +++ src/enums/unitSystem.ts | 4 + src/errors.ts | 13 +++ src/index.ts | 15 +-- src/models/activityStats.ts | 15 +++ src/models/activityTotal.ts | 8 ++ src/models/activityZone.ts | 13 +++ src/models/comment.ts | 9 ++ src/models/detailedActivity.ts | 58 ++++++++++ src/models/detailedAthlete.ts | 31 ++++++ src/models/detailedGear.ts | 13 +++ src/models/detailedSegmentEffort.ts | 25 +++++ src/models/heartRateZoneRanges.ts | 6 ++ src/models/index.ts | 28 +++++ src/models/lap.ts | 22 ++++ src/models/metaActivity.ts | 3 + src/models/metaAthlete.ts | 3 + src/models/photoSummary.ts | 6 ++ src/models/photoSummaryPrimary.ts | 6 ++ src/models/polylineMap.ts | 5 + src/models/powerZoneRanges.ts | 5 + src/models/split.ts | 9 ++ src/models/stream.ts | 18 ++++ src/models/streamSet.ts | 15 +++ src/models/summaryActivity.ts | 39 +++++++ src/models/summaryAthlete.ts | 19 ++++ src/models/summaryClub.ts | 19 ++++ src/models/summaryGear.ts | 9 ++ src/models/summarySegment.ts | 22 ++++ src/models/summarySegmentEffort.ts | 8 ++ src/models/timedZoneRange.ts | 5 + src/models/zoneRange.ts | 4 + src/models/zones.ts | 6 ++ src/oauth.ts | 9 -- src/request.ts | 89 ++++++++++++---- src/resources/activities.ts | 159 ++++++++++++++++++++++++++++ src/resources/index.ts | 1 + src/types.ts | 18 ++++ tests/error.test.ts | 39 +++++++ 48 files changed, 855 insertions(+), 36 deletions(-) create mode 100644 src/enums/activityType.ts create mode 100644 src/enums/activityZoneType.ts create mode 100644 src/enums/followerStatus.ts create mode 100644 src/enums/index.ts create mode 100644 src/enums/resourceState.ts create mode 100644 src/enums/sex.ts create mode 100644 src/enums/sportType.ts create mode 100644 src/enums/streamKeys.ts create mode 100644 src/enums/unitSystem.ts create mode 100644 src/errors.ts create mode 100644 src/models/activityStats.ts create mode 100644 src/models/activityTotal.ts create mode 100644 src/models/activityZone.ts create mode 100644 src/models/comment.ts create mode 100644 src/models/detailedActivity.ts create mode 100644 src/models/detailedAthlete.ts create mode 100644 src/models/detailedGear.ts create mode 100644 src/models/detailedSegmentEffort.ts create mode 100644 src/models/heartRateZoneRanges.ts create mode 100644 src/models/index.ts create mode 100644 src/models/lap.ts create mode 100644 src/models/metaActivity.ts create mode 100644 src/models/metaAthlete.ts create mode 100644 src/models/photoSummary.ts create mode 100644 src/models/photoSummaryPrimary.ts create mode 100644 src/models/polylineMap.ts create mode 100644 src/models/powerZoneRanges.ts create mode 100644 src/models/split.ts create mode 100644 src/models/stream.ts create mode 100644 src/models/streamSet.ts create mode 100644 src/models/summaryActivity.ts create mode 100644 src/models/summaryAthlete.ts create mode 100644 src/models/summaryClub.ts create mode 100644 src/models/summaryGear.ts create mode 100644 src/models/summarySegment.ts create mode 100644 src/models/summarySegmentEffort.ts create mode 100644 src/models/timedZoneRange.ts create mode 100644 src/models/zoneRange.ts create mode 100644 src/models/zones.ts delete mode 100644 src/oauth.ts create mode 100644 src/resources/activities.ts create mode 100644 src/resources/index.ts create mode 100644 src/types.ts create mode 100644 tests/error.test.ts diff --git a/package-lock.json b/package-lock.json index 1f7ad65..042daee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2129,6 +2129,29 @@ "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==", "dev": true }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", diff --git a/package.json b/package.json index 635c1dc..d186b6e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "type": "git", "url": "git+https://github.com/rfoel/strava.git" }, - "keywords": [], + "keywords": [ + "strava", + "api", + "client", + "node" + ], "author": "Rafael Franco ", "license": "MIT", "bugs": { @@ -33,6 +38,7 @@ "@semantic-release/github": "^7.1.1", "@semantic-release/npm": "^7.0.6", "@semantic-release/release-notes-generator": "^9.0.1", + "@types/node-fetch": "^2.5.7", "jest": "^26.6.3", "microbundle": "^0.12.4", "nock": "^13.0.5", diff --git a/src/enums/activityType.ts b/src/enums/activityType.ts new file mode 100644 index 0000000..3c947db --- /dev/null +++ b/src/enums/activityType.ts @@ -0,0 +1,39 @@ +export enum ActivityType { + AlpineSki = 'AlpineSki', + BackcountrySki = 'BackcountrySki', + Canoeing = 'Canoeing', + Crossfit = 'Crossfit', + EBikeRide = 'EBikeRide', + Elliptical = 'Elliptical', + Golf = 'Golf', + Handcycle = 'Handcycle', + Hike = 'Hike', + IceSkate = 'IceSkate', + InlineSkate = 'InlineSkate', + Kayaking = 'Kayaking', + Kitesurf = 'Kitesurf', + NordicSki = 'NordicSki', + Ride = 'Ride', + RockClimbing = 'RockClimbing', + RollerSki = 'RollerSki', + Rowing = 'Rowing', + Run = 'Run', + Sail = 'Sail', + Skateboard = 'Skateboard', + Snowboard = 'Snowboard', + Snowshoe = 'Snowshoe', + Soccer = 'Soccer', + StairStepper = 'StairStepper', + StandUpPaddling = 'StandUpPaddling', + Surfing = 'Surfing', + Swim = 'Swim', + Velomobile = 'Velomobile', + VirtualRide = 'VirtualRide', + VirtualRun = 'VirtualRun', + Walk = 'Walk', + WeightTraining = 'WeightTraining', + Wheelchair = 'Wheelchair', + Windsurf = 'Windsurf', + Workout = 'Workout', + Yoga = 'Yoga', +} diff --git a/src/enums/activityZoneType.ts b/src/enums/activityZoneType.ts new file mode 100644 index 0000000..0feeab7 --- /dev/null +++ b/src/enums/activityZoneType.ts @@ -0,0 +1,4 @@ +export enum ActivityZoneType { + Heartrate = 'heartrate', + Power = 'power', +} diff --git a/src/enums/followerStatus.ts b/src/enums/followerStatus.ts new file mode 100644 index 0000000..2f81bf1 --- /dev/null +++ b/src/enums/followerStatus.ts @@ -0,0 +1,5 @@ +export enum FollowerStatus { + Pending = 'pending', + Accepted = 'accepted', + Blocked = 'blocked', +} diff --git a/src/enums/index.ts b/src/enums/index.ts new file mode 100644 index 0000000..bd3c039 --- /dev/null +++ b/src/enums/index.ts @@ -0,0 +1,8 @@ +export * from './activityType' +export * from './activityZoneType' +export * from './followerStatus' +export * from './resourceState' +export * from './sex' +export * from './sportType' +export * from './streamKeys' +export * from './unitSystem' diff --git a/src/enums/resourceState.ts b/src/enums/resourceState.ts new file mode 100644 index 0000000..1ee51df --- /dev/null +++ b/src/enums/resourceState.ts @@ -0,0 +1,5 @@ +export enum ResourceState { + Meta = 1, + Summary = 2, + Detail = 3, +} diff --git a/src/enums/sex.ts b/src/enums/sex.ts new file mode 100644 index 0000000..224d7ad --- /dev/null +++ b/src/enums/sex.ts @@ -0,0 +1,4 @@ +export enum Sex { + Female = 'F', + Male = 'M', +} diff --git a/src/enums/sportType.ts b/src/enums/sportType.ts new file mode 100644 index 0000000..a6bf7eb --- /dev/null +++ b/src/enums/sportType.ts @@ -0,0 +1,6 @@ +export enum SportType { + Cycling = 'cycling', + Running = 'running', + Triathlon = 'triathlon', + Other = 'other', +} diff --git a/src/enums/streamKeys.ts b/src/enums/streamKeys.ts new file mode 100644 index 0000000..ec9f7d4 --- /dev/null +++ b/src/enums/streamKeys.ts @@ -0,0 +1,13 @@ +export enum StreamKeys { + Time = 'time', + Distance = 'distance', + LatLng = 'latlng', + Altitude = 'altitude', + VelocitySmooth = 'velocity_smooth', + Heartrate = 'heartrate', + Cadence = 'cadence', + Watts = 'watts', + Temp = 'temp', + Moving = 'moving', + GradeSmooth = 'grade_smooth', +} diff --git a/src/enums/unitSystem.ts b/src/enums/unitSystem.ts new file mode 100644 index 0000000..0e58269 --- /dev/null +++ b/src/enums/unitSystem.ts @@ -0,0 +1,4 @@ +export enum UnitSystem { + Feet = 'feet', + Meters = 'meters', +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..fc8122a --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,13 @@ +export class StravaError { + errors: any + message: string + status: number + statusText: string + + constructor(error: Response, data: any) { + this.errors = data.errors + this.message = data.message + this.status = error.status + this.statusText = error.statusText + } +} diff --git a/src/index.ts b/src/index.ts index 77e7fc0..3e1204b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,15 @@ -import { OAuth } from './oauth' import { Request } from './request' +import { Activities } from './resources' +import { Config } from './types' -export default class Strava { +export class Strava { private readonly request: Request - oauth: OAuth + activities: Activities - constructor(apiKey: string) { - this.request = new Request() - this.oauth = new OAuth(this.request) + constructor(config: Config) { + this.request = new Request(config) + this.activities = new Activities(this.request) } } + +export default Strava diff --git a/src/models/activityStats.ts b/src/models/activityStats.ts new file mode 100644 index 0000000..c85cc6f --- /dev/null +++ b/src/models/activityStats.ts @@ -0,0 +1,15 @@ +import { ActivityTotal } from '.' + +export interface ActivityStats { + biggest_ride_distance: number + biggest_climb_elevation_gain: number + recent_ride_totals: ActivityTotal + recent_run_totals: ActivityTotal + recent_swim_totals: ActivityTotal + ytd_ride_totals: ActivityTotal + ytd_run_totals: ActivityTotal + ytd_swim_totals: ActivityTotal + all_ride_totals: ActivityTotal + all_run_totals: ActivityTotal + all_swim_totals: ActivityTotal +} diff --git a/src/models/activityTotal.ts b/src/models/activityTotal.ts new file mode 100644 index 0000000..26c2ace --- /dev/null +++ b/src/models/activityTotal.ts @@ -0,0 +1,8 @@ +export interface ActivityTotal { + count: number + distance: number + moving_time: number + elapsed_time: number + elevation_gain: number + achievement_count: number +} diff --git a/src/models/activityZone.ts b/src/models/activityZone.ts new file mode 100644 index 0000000..3d953a3 --- /dev/null +++ b/src/models/activityZone.ts @@ -0,0 +1,13 @@ +import { ActivityZoneType } from '../enums' + +import { TimedZoneRange } from '.' + +export interface ActivityZone { + score: number + distribution_buckets: TimedZoneRange[] + type: ActivityZoneType + sensor_based: boolean + points: number + custom_zones: boolean + max: number +} diff --git a/src/models/comment.ts b/src/models/comment.ts new file mode 100644 index 0000000..3fc0328 --- /dev/null +++ b/src/models/comment.ts @@ -0,0 +1,9 @@ +import { SummaryAthlete } from '.' + +export interface Comment { + id: number + activity_id: number + text: string + athlete: SummaryAthlete + created_at: Date +} diff --git a/src/models/detailedActivity.ts b/src/models/detailedActivity.ts new file mode 100644 index 0000000..34ead8e --- /dev/null +++ b/src/models/detailedActivity.ts @@ -0,0 +1,58 @@ +import { ActivityType } from '../enums' + +import { + DetailedSegmentEffort, + Lap, + MetaAthlete, + PhotoSummary, + PolylineMap, + Split, + SummaryGear, +} from '.' + +export interface DetailedActivity { + id: number + external_id: string + upload_id: number + athlete: MetaAthlete + name: string + distance: number + moving_time: number + elapsed_time: number + total_elevation_gain: number + elev_high: number + elev_low: number + type: ActivityType + start_date: Date + start_date_local: Date + timezone: string + start_latlng: number[] + end_latlng: number[] + achievement_count: number + kudos_count: number + comment_count: number + athlete_count: number + photo_count: number + total_photo_count: number + map: PolylineMap + trainer: boolean + commute: boolean + manual: boolean + private: boolean + flagged: boolean + workout_type: number + average_speed: number + max_speed: number + has_kudoed: boolean + description: string + photos: PhotoSummary + gear: SummaryGear + calories: number + segment_efforts: DetailedSegmentEffort[] + device_name: string + embed_token: string + splits_metric: Split + splits_standard: Split + laps: Lap[] + best_efforts: DetailedSegmentEffort[] +} diff --git a/src/models/detailedAthlete.ts b/src/models/detailedAthlete.ts new file mode 100644 index 0000000..f9a21da --- /dev/null +++ b/src/models/detailedAthlete.ts @@ -0,0 +1,31 @@ +import { FollowerStatus, ResourceState, UnitSystem } from '../enums' + +import { SummaryClub, SummaryGear } from '.' + +export interface DetailedAthlete { + id: number + resource_state: ResourceState + firstname: string + lastname: string + profile_medium: string + profile: string + city: string + state: string + country: string + sex: string + friend: FollowerStatus + follower: FollowerStatus + premium: boolean + created_at: Date + updated_at: Date + follower_count: number + friend_count: number + mutual_friend_count: number + measurement_preference: UnitSystem + email: string + ftp: number + weight: number + clubs: SummaryClub + bikes: SummaryGear + shoes: SummaryGear +} diff --git a/src/models/detailedGear.ts b/src/models/detailedGear.ts new file mode 100644 index 0000000..79b7f28 --- /dev/null +++ b/src/models/detailedGear.ts @@ -0,0 +1,13 @@ +import { ResourceState } from '../enums' + +export interface DetailedGear { + id: string + resource_state: ResourceState + primary: boolean + name: string + distance: number + brand_name: string + model_name: string + frame_type: number + description: string +} diff --git a/src/models/detailedSegmentEffort.ts b/src/models/detailedSegmentEffort.ts new file mode 100644 index 0000000..7e6e133 --- /dev/null +++ b/src/models/detailedSegmentEffort.ts @@ -0,0 +1,25 @@ +import { MetaActivity, MetaAthlete, SummarySegment } from '.' + +export interface DetailedSegmentEffort { + id: number + elapsed_time: number + start_date: Date + start_date_local: Date + distance: number + is_kom: boolean + name: string + activity: MetaActivity + athlete: MetaAthlete + moving_time: number + start_index: number + end_index: number + average_cadence: number + average_watts: number + device_watts: boolean + average_heartrate: number + max_heartrate: number + segment: SummarySegment + kom_rank: number + pr_rank: number + hidden: boolean +} diff --git a/src/models/heartRateZoneRanges.ts b/src/models/heartRateZoneRanges.ts new file mode 100644 index 0000000..4febe04 --- /dev/null +++ b/src/models/heartRateZoneRanges.ts @@ -0,0 +1,6 @@ +import { ZoneRange } from '.' + +export interface HeartRateZoneRanges { + custom_zones: boolean + zones: ZoneRange[] +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..9390d9c --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,28 @@ +export * from './activityStats' +export * from './activityTotal' +export * from './activityZone' +export * from './comment' +export * from './detailedActivity' +export * from './detailedAthlete' +export * from './detailedGear' +export * from './detailedSegmentEffort' +export * from './heartRateZoneRanges' +export * from './lap' +export * from './metaActivity' +export * from './metaAthlete' +export * from './photoSummary' +export * from './photoSummaryPrimary' +export * from './polylineMap' +export * from './powerZoneRanges' +export * from './split' +export * from './stream' +export * from './streamSet' +export * from './summaryActivity' +export * from './summaryAthlete' +export * from './summaryClub' +export * from './summaryGear' +export * from './summarySegment' +export * from './summarySegmentEffort' +export * from './timedZoneRange' +export * from './zoneRange' +export * from './zones' diff --git a/src/models/lap.ts b/src/models/lap.ts new file mode 100644 index 0000000..130071b --- /dev/null +++ b/src/models/lap.ts @@ -0,0 +1,22 @@ +import { MetaActivity, MetaAthlete } from '.' + +export interface Lap { + id: number + activity: MetaActivity + athlete: MetaAthlete + average_cadence: number + average_speed: number + distance: number + elapsed_time: number + start_index: number + end_index: number + lap_index: number + max_speed: number + moving_time: number + name: string + pace_zone: number + split: number + start_date: Date + start_date_local: Date + total_elevation_gain: number +} diff --git a/src/models/metaActivity.ts b/src/models/metaActivity.ts new file mode 100644 index 0000000..c417c39 --- /dev/null +++ b/src/models/metaActivity.ts @@ -0,0 +1,3 @@ +export interface MetaActivity { + id: number +} diff --git a/src/models/metaAthlete.ts b/src/models/metaAthlete.ts new file mode 100644 index 0000000..53af816 --- /dev/null +++ b/src/models/metaAthlete.ts @@ -0,0 +1,3 @@ +export interface MetaAthlete { + id: number +} diff --git a/src/models/photoSummary.ts b/src/models/photoSummary.ts new file mode 100644 index 0000000..d92fa5c --- /dev/null +++ b/src/models/photoSummary.ts @@ -0,0 +1,6 @@ +import { PhotoSummaryPrimary } from '.' + +export interface PhotoSummary { + count: number + primary: PhotoSummaryPrimary +} diff --git a/src/models/photoSummaryPrimary.ts b/src/models/photoSummaryPrimary.ts new file mode 100644 index 0000000..9da7ad1 --- /dev/null +++ b/src/models/photoSummaryPrimary.ts @@ -0,0 +1,6 @@ +export interface PhotoSummaryPrimary { + id: number + source: number + unique_id: string + urls: string +} diff --git a/src/models/polylineMap.ts b/src/models/polylineMap.ts new file mode 100644 index 0000000..7c66553 --- /dev/null +++ b/src/models/polylineMap.ts @@ -0,0 +1,5 @@ +export interface PolylineMap { + id: string + polyline: string + summary_polyline: string +} diff --git a/src/models/powerZoneRanges.ts b/src/models/powerZoneRanges.ts new file mode 100644 index 0000000..5ace6fa --- /dev/null +++ b/src/models/powerZoneRanges.ts @@ -0,0 +1,5 @@ +import { ZoneRange } from '.' + +export interface PowerZoneRanges { + zones: ZoneRange[] +} diff --git a/src/models/split.ts b/src/models/split.ts new file mode 100644 index 0000000..b7edffa --- /dev/null +++ b/src/models/split.ts @@ -0,0 +1,9 @@ +export interface Split { + average_speed: number + distance: number + elapsed_time: number + elevation_difference: number + pace_zone: number + moving_time: number + split: number +} diff --git a/src/models/stream.ts b/src/models/stream.ts new file mode 100644 index 0000000..1457723 --- /dev/null +++ b/src/models/stream.ts @@ -0,0 +1,18 @@ +export interface Stream { + type: + | 'time' + | 'distance' + | 'latlng' + | 'altitude' + | 'velocity_smooth' + | 'heartrate' + | 'cadence' + | 'watts' + | 'temp' + | 'moving' + | 'grade_smooth' + original_size: number + resolution: 'low' | 'medium' | 'high' + series_type: 'distance' | 'time' + data: number[] +} diff --git a/src/models/streamSet.ts b/src/models/streamSet.ts new file mode 100644 index 0000000..83ab48d --- /dev/null +++ b/src/models/streamSet.ts @@ -0,0 +1,15 @@ +import { Stream } from '.' + +export interface StreamSet { + time: Stream + distance: Stream + latlng: Stream + altitude: Stream + velocity_smooth: Stream + heartrate: Stream + cadence: Stream + watts: Stream + temp: Stream + moving: Stream + grade_smooth: Stream +} diff --git a/src/models/summaryActivity.ts b/src/models/summaryActivity.ts new file mode 100644 index 0000000..db1a4bc --- /dev/null +++ b/src/models/summaryActivity.ts @@ -0,0 +1,39 @@ +import { ActivityType } from '../enums' + +import { MetaAthlete, PolylineMap } from '.' + +export interface SummaryActivity { + id: number + external_id: string + upload_id: number + athlete: MetaAthlete + name: string + distance: number + moving_time: number + elapsed_time: number + total_elevation_gain: number + elev_high: number + elev_low: number + type: ActivityType + start_date: Date + start_date_local: Date + timezone: string + start_latlng: number[] + end_latlng: number[] + achievement_count: number + kudos_count: number + comment_count: number + athlete_count: number + photo_count: number + total_photo_count: number + map: PolylineMap + trainer: boolean + commute: boolean + manual: boolean + private: boolean + flagged: boolean + workout_type: number + average_speed: number + max_speed: number + has_kudoed: boolean +} diff --git a/src/models/summaryAthlete.ts b/src/models/summaryAthlete.ts new file mode 100644 index 0000000..adca7e9 --- /dev/null +++ b/src/models/summaryAthlete.ts @@ -0,0 +1,19 @@ +import { ResourceState, Sex } from '../enums' + +export interface SummaryAthlete { + id: number + resource_state: ResourceState + firstname: string + lastname: string + profile_medium: string + profile: string + city: string + state: string + country: string + sex: Sex + friend: string + follower: string + premium: boolean + created_at: Date + updated_at: Date +} diff --git a/src/models/summaryClub.ts b/src/models/summaryClub.ts new file mode 100644 index 0000000..ad24523 --- /dev/null +++ b/src/models/summaryClub.ts @@ -0,0 +1,19 @@ +import { ResourceState, SportType } from '../enums' + +export interface SummaryClub { + id: number + resource_state: ResourceState + name: string + profile_medium: string + cover_photo: string + cover_photo_small: string + sport_type: SportType + city: string + state: string + country: string + private: boolean + member_count: number + featured: boolean + verified: boolean + url: string +} diff --git a/src/models/summaryGear.ts b/src/models/summaryGear.ts new file mode 100644 index 0000000..c53e72b --- /dev/null +++ b/src/models/summaryGear.ts @@ -0,0 +1,9 @@ +import { ResourceState } from '../enums' + +export interface SummaryGear { + id: string + resource_state: ResourceState + primary: boolean + name: string + distance: number +} diff --git a/src/models/summarySegment.ts b/src/models/summarySegment.ts new file mode 100644 index 0000000..52fc3d4 --- /dev/null +++ b/src/models/summarySegment.ts @@ -0,0 +1,22 @@ +import { ActivityType } from '../enums' + +import { SummarySegmentEffort } from '.' + +export interface SummarySegment { + id: number + name: string + activity_type: ActivityType + distance: number + average_grade: number + maximum_grade: number + elevation_high: number + elevation_low: number + start_latlng: number[] + end_latlng: number[] + climb_category: number + city: string + state: string + country: string + private: boolean + athlete_pr_effort: SummarySegmentEffort +} diff --git a/src/models/summarySegmentEffort.ts b/src/models/summarySegmentEffort.ts new file mode 100644 index 0000000..12e8d22 --- /dev/null +++ b/src/models/summarySegmentEffort.ts @@ -0,0 +1,8 @@ +export interface SummarySegmentEffort { + id: number + elapsed_time: number + start_date: Date + start_date_local: Date + distance: number + is_kom: boolean +} diff --git a/src/models/timedZoneRange.ts b/src/models/timedZoneRange.ts new file mode 100644 index 0000000..2d720d8 --- /dev/null +++ b/src/models/timedZoneRange.ts @@ -0,0 +1,5 @@ +export interface TimedZoneRange { + min: number + max: number + time: number +} diff --git a/src/models/zoneRange.ts b/src/models/zoneRange.ts new file mode 100644 index 0000000..7508259 --- /dev/null +++ b/src/models/zoneRange.ts @@ -0,0 +1,4 @@ +export interface ZoneRange { + min: number + max: number +} diff --git a/src/models/zones.ts b/src/models/zones.ts new file mode 100644 index 0000000..8ba1162 --- /dev/null +++ b/src/models/zones.ts @@ -0,0 +1,6 @@ +import { HeartRateZoneRanges, PowerZoneRanges } from '.'; + +export interface Zones { + heart_rate: HeartRateZoneRanges; + power: PowerZoneRanges; +} diff --git a/src/oauth.ts b/src/oauth.ts deleted file mode 100644 index 018babb..0000000 --- a/src/oauth.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Request } from './request' - -export class OAuth { - private readonly request: Request - - constructor(request) { - this.request = request - } -} diff --git a/src/request.ts b/src/request.ts index 84f8c92..525e108 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,36 +1,85 @@ import fetch from 'node-fetch' +import { StravaError } from './errors' +import { Config, RefreshTokenRequest, RefreshTokenResponse } from './types' + type RequestParams = { - uri: string - params: any + query?: any + body?: any } export class Request { - request: any - - constructor(accessToken: string) { - this.request = ({ uri, params }: RequestParams): void => { - const query: string = new URLSearchParams(params).toString() - return fetch(`https://www.strava.com/api/v3/${uri}?${query}`, { - headers: { Bearer: accessToken }, - }) + config: Config + response: RefreshTokenResponse + + constructor(config: Config) { + this.config = config + } + + private async getAccessToken(): Promise { + if ( + !this.response || + this.response?.expires_at < new Date().getTime() / 1000 + ) { + const query: string = new URLSearchParams({ + client_id: this.config.client_id, + client_secret: this.config.client_secret, + refresh_token: this.config.refresh_token, + grant_type: 'refresh_token', + }).toString() + + const response = await fetch( + `https://www.strava.com/oauth/token?${query}`, + { + method: 'post', + }, + ) + + if (!response.ok) { + throw response + } + + this.response = await response.json() } + return this.response } public async makeApiRequest( + method: string, uri: string, - params: any, + params: RequestParams, ): Promise { - const response = await this.request({ - uri, - params, - }) + try { + await this.getAccessToken() - if (!response.ok) { - throw response - } + const query: string = new URLSearchParams(params.query).toString() + const response = await fetch( + `https://www.strava.com/api/v3${uri}?${query}`, + { + body: params.body, + method, + headers: { Authorization: `Bearer ${this.response.access_token}` }, + }, + ) - const { data } = await response.json() - return data + if (!response.ok) { + throw response + } + + return await response.json() + } catch (error) { + const data = await error.json() + switch (error.status) { + case 400: + case 401: + case 403: + case 404: + case 429: + case 500: + throw new StravaError(error, data) + default: + throw error + } + } } } diff --git a/src/resources/activities.ts b/src/resources/activities.ts new file mode 100644 index 0000000..fbb17f3 --- /dev/null +++ b/src/resources/activities.ts @@ -0,0 +1,159 @@ +import { ActivityType } from '../enums' +import { + ActivityZone, + Comment, + DetailedActivity, + Lap, + SummaryActivity, + SummaryAthlete, +} from '../models' +import { Request } from '../request' + +type CreateActivityRequest = { + name: string + type: ActivityType + start_date_local: Date + elapsed_time: number + description?: string + distance?: number + trainer?: number + commute?: number +} + +type GetActivityByIdRequest = { + id: number + include_all_efforts?: boolean +} + +type GetCommentsByActivityIdRequest = { + id: number + page?: number + per_page?: number +} + +type GetKudoersByActivityIdRequest = { + id: number + page?: number + per_page?: number +} + +type GetLapsByActivityIdRequest = { + id: number +} + +type GetZonesByActivityIdRequest = { + id: number +} + +type GetLoggedInAthleteActivitiesRequest = { + before?: number + after?: number + page?: number + per_page?: number +} + +type UpdateActivityByIdRequest = { + id: number + name: string + type: ActivityType + start_date_local: Date + elapsed_time: number + description?: string + distance?: number + trainer?: number + commute?: number +} + +export class Activities { + private readonly request: Request + + constructor(request) { + this.request = request + } + + async createActivity( + params: CreateActivityRequest, + ): Promise { + return await this.request.makeApiRequest( + 'post', + '/activities', + { body: params }, + ) + } + + async getActivityById( + params: GetActivityByIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'get', + `/activities/${id}`, + { query }, + ) + } + + async getCommentsByActivityId( + params: GetCommentsByActivityIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'get', + `/activities/${id}/comments`, + { query }, + ) + } + + async getKudoersByActivityId( + params: GetKudoersByActivityIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'get', + `/activities/${id}/kudos`, + { query }, + ) + } + + async getLapsByActivityId( + params: GetLapsByActivityIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'get', + `/activities/${id}/laps`, + { query }, + ) + } + + async getLoggedInAthleteActivities( + params?: GetLoggedInAthleteActivitiesRequest, + ): Promise { + return await this.request.makeApiRequest( + 'get', + '/athlete/activities', + { query: params }, + ) + } + + async getZonesByActivityId( + params: GetZonesByActivityIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'get', + `/activities/${id}/zones`, + { query }, + ) + } + + async updateActivityById( + params: UpdateActivityByIdRequest, + ): Promise { + const { id, ...query } = params + return await this.request.makeApiRequest( + 'put', + `/activities/${id}`, + { query }, + ) + } +} diff --git a/src/resources/index.ts b/src/resources/index.ts new file mode 100644 index 0000000..8b9870b --- /dev/null +++ b/src/resources/index.ts @@ -0,0 +1 @@ +export * from './activities' diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..986e327 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,18 @@ +export interface Config { + client_id: string + client_secret: string + refresh_token: string +} + +export interface RefreshTokenRequest { + client_id: string + client_secret: string + refresh_token: string +} + +export interface RefreshTokenResponse { + access_token: string + expires_at: number + expires_in: number + refresh_token: string +} diff --git a/tests/error.test.ts b/tests/error.test.ts new file mode 100644 index 0000000..625e260 --- /dev/null +++ b/tests/error.test.ts @@ -0,0 +1,39 @@ +import * as nock from 'nock' +import Strava from '../src' +import { StravaError } from '../src/errors' + +const scope = nock('https://www.strava.com') + +let strava: Strava + +const mockAuth = () => + scope + .post('/oauth/token') + .query(true) + .reply(200, { + access_token: 'abc', + expires_at: new Date().getTime() + 21600 / 1000, + expires_in: 21600, + refresh_token: 'def', + }) + +beforeEach(() => { + strava = new Strava({ + client_id: '123', + client_secret: 'abc', + refresh_token: 'def', + }) +}) + +it('should throw bad request error', async () => { + mockAuth() + scope.get('/api/v3/athlete/activities').query(true).reply(404, { + status: 404, + statusText: 'Not Found', + }) + try { + await strava.activities.getLoggedInAthleteActivities() + } catch (error) { + expect(error).toEqual(new StravaError(error, {})) + } +})