@nativesquare/soma 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -19
- package/dist/client/index.d.ts +158 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +165 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +37 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/public.d.ts +3 -3
- package/dist/component/schema.d.ts +18 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +10 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +88 -0
- package/dist/component/strava.d.ts.map +1 -0
- package/dist/component/strava.js +318 -0
- package/dist/component/strava.js.map +1 -0
- package/dist/component/validators/activity.d.ts +4 -4
- package/dist/component/validators/samples.d.ts +2 -2
- package/dist/strava/activity.d.ts +121 -0
- package/dist/strava/activity.d.ts.map +1 -0
- package/dist/strava/activity.js +201 -0
- package/dist/strava/activity.js.map +1 -0
- package/dist/strava/athlete.d.ts +34 -0
- package/dist/strava/athlete.d.ts.map +1 -0
- package/dist/strava/athlete.js +39 -0
- package/dist/strava/athlete.js.map +1 -0
- package/dist/strava/auth.d.ts +103 -0
- package/dist/strava/auth.d.ts.map +1 -0
- package/dist/strava/auth.js +111 -0
- package/dist/strava/auth.js.map +1 -0
- package/dist/strava/client.d.ts +93 -0
- package/dist/strava/client.d.ts.map +1 -0
- package/dist/strava/client.js +158 -0
- package/dist/strava/client.js.map +1 -0
- package/dist/strava/index.d.ts +13 -0
- package/dist/strava/index.d.ts.map +1 -0
- package/dist/strava/index.js +17 -0
- package/dist/strava/index.js.map +1 -0
- package/dist/strava/maps/sport-type.d.ts +7 -0
- package/dist/strava/maps/sport-type.d.ts.map +1 -0
- package/dist/strava/maps/sport-type.js +84 -0
- package/dist/strava/maps/sport-type.js.map +1 -0
- package/dist/strava/sync.d.ts +104 -0
- package/dist/strava/sync.d.ts.map +1 -0
- package/dist/strava/sync.js +87 -0
- package/dist/strava/sync.js.map +1 -0
- package/dist/strava/types.d.ts +266 -0
- package/dist/strava/types.d.ts.map +1 -0
- package/dist/strava/types.js +8 -0
- package/dist/strava/types.js.map +1 -0
- package/package.json +5 -1
- package/src/client/index.ts +212 -4
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +49 -0
- package/src/component/schema.ts +11 -0
- package/src/component/strava.ts +383 -0
- package/src/strava/activity.test.ts +415 -0
- package/src/strava/activity.ts +276 -0
- package/src/strava/athlete.test.ts +139 -0
- package/src/strava/athlete.ts +47 -0
- package/src/strava/auth.test.ts +78 -0
- package/src/strava/auth.ts +185 -0
- package/src/strava/client.ts +212 -0
- package/src/strava/index.ts +54 -0
- package/src/strava/maps/sport-type.test.ts +69 -0
- package/src/strava/maps/sport-type.ts +99 -0
- package/src/strava/sync.ts +168 -0
- package/src/strava/types.ts +361 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ─── @nativesquare/soma/strava ───────────────────────────────────────────────
|
|
2
|
+
// Strava API → Soma schema transformers, API client, OAuth helpers, and sync.
|
|
3
|
+
//
|
|
4
|
+
// Pure TypeScript with zero runtime dependencies (uses global `fetch`).
|
|
5
|
+
// Compatible with Convex actions and any modern runtime.
|
|
6
|
+
|
|
7
|
+
// ── Transformers ─────────────────────────────────────────────────────────────
|
|
8
|
+
export { transformActivity } from "./activity.js";
|
|
9
|
+
export type { ActivityData } from "./activity.js";
|
|
10
|
+
|
|
11
|
+
export { transformAthlete } from "./athlete.js";
|
|
12
|
+
export type { AthleteData } from "./athlete.js";
|
|
13
|
+
|
|
14
|
+
// ── Enum Maps ────────────────────────────────────────────────────────────────
|
|
15
|
+
export { mapSportType } from "./maps/sport-type.js";
|
|
16
|
+
|
|
17
|
+
// ── API Client ───────────────────────────────────────────────────────────────
|
|
18
|
+
export { StravaClient, StravaApiError } from "./client.js";
|
|
19
|
+
export type { StravaClientOptions } from "./client.js";
|
|
20
|
+
|
|
21
|
+
// ── OAuth Helpers ────────────────────────────────────────────────────────────
|
|
22
|
+
export { buildAuthUrl, exchangeCode, refreshToken } from "./auth.js";
|
|
23
|
+
export type {
|
|
24
|
+
BuildAuthUrlOptions,
|
|
25
|
+
ExchangeCodeOptions,
|
|
26
|
+
RefreshTokenOptions,
|
|
27
|
+
} from "./auth.js";
|
|
28
|
+
|
|
29
|
+
// ── Sync Helpers ─────────────────────────────────────────────────────────────
|
|
30
|
+
export { syncActivities, syncAthlete } from "./sync.js";
|
|
31
|
+
export type {
|
|
32
|
+
SyncActivitiesOptions,
|
|
33
|
+
SyncActivitiesResult,
|
|
34
|
+
SyncAthleteOptions,
|
|
35
|
+
} from "./sync.js";
|
|
36
|
+
|
|
37
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
38
|
+
export type {
|
|
39
|
+
DetailedActivity,
|
|
40
|
+
DetailedAthlete,
|
|
41
|
+
Lap,
|
|
42
|
+
OAuthTokenResponse,
|
|
43
|
+
PolylineMap,
|
|
44
|
+
SegmentEffort,
|
|
45
|
+
Split,
|
|
46
|
+
Stream,
|
|
47
|
+
StreamSet,
|
|
48
|
+
StravaSportType,
|
|
49
|
+
SummaryActivity,
|
|
50
|
+
SummaryAthlete,
|
|
51
|
+
SummaryClub,
|
|
52
|
+
SummaryGear,
|
|
53
|
+
SummarySegment,
|
|
54
|
+
} from "./types.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mapSportType } from "./sport-type.js";
|
|
3
|
+
|
|
4
|
+
describe("mapSportType", () => {
|
|
5
|
+
it("maps cycling sport types to Terra Biking (1)", () => {
|
|
6
|
+
expect(mapSportType("Ride")).toBe(1);
|
|
7
|
+
expect(mapSportType("MountainBikeRide")).toBe(1);
|
|
8
|
+
expect(mapSportType("GravelRide")).toBe(1);
|
|
9
|
+
expect(mapSportType("EBikeRide")).toBe(1);
|
|
10
|
+
expect(mapSportType("EMountainBikeRide")).toBe(1);
|
|
11
|
+
expect(mapSportType("VirtualRide")).toBe(1);
|
|
12
|
+
expect(mapSportType("Velomobile")).toBe(1);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("maps running sport types to Terra Running (8)", () => {
|
|
16
|
+
expect(mapSportType("Run")).toBe(8);
|
|
17
|
+
expect(mapSportType("TrailRun")).toBe(8);
|
|
18
|
+
expect(mapSportType("VirtualRun")).toBe(8);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("maps Walk to Terra Walking (7)", () => {
|
|
22
|
+
expect(mapSportType("Walk")).toBe(7);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("maps Swim to Terra Swimming (82)", () => {
|
|
26
|
+
expect(mapSportType("Swim")).toBe(82);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("maps Hike to Terra Hiking (35)", () => {
|
|
30
|
+
expect(mapSportType("Hike")).toBe(35);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("maps snow sports correctly", () => {
|
|
34
|
+
expect(mapSportType("AlpineSki")).toBe(66);
|
|
35
|
+
expect(mapSportType("NordicSki")).toBe(67);
|
|
36
|
+
expect(mapSportType("Snowboard")).toBe(73);
|
|
37
|
+
expect(mapSportType("Snowshoe")).toBe(74);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("maps water sports correctly", () => {
|
|
41
|
+
expect(mapSportType("Rowing")).toBe(53);
|
|
42
|
+
expect(mapSportType("Kayaking")).toBe(40);
|
|
43
|
+
expect(mapSportType("Sail")).toBe(59);
|
|
44
|
+
expect(mapSportType("Surfing")).toBe(81);
|
|
45
|
+
expect(mapSportType("Kitesurf")).toBe(41);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("maps racket sports correctly", () => {
|
|
49
|
+
expect(mapSportType("Tennis")).toBe(87);
|
|
50
|
+
expect(mapSportType("TableTennis")).toBe(85);
|
|
51
|
+
expect(mapSportType("Badminton")).toBe(10);
|
|
52
|
+
expect(mapSportType("Squash")).toBe(76);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("maps gym/fitness activities correctly", () => {
|
|
56
|
+
expect(mapSportType("WeightTraining")).toBe(80);
|
|
57
|
+
expect(mapSportType("Crossfit")).toBe(113);
|
|
58
|
+
expect(mapSportType("Yoga")).toBe(100);
|
|
59
|
+
expect(mapSportType("Pilates")).toBe(49);
|
|
60
|
+
expect(mapSportType("HighIntensityIntervalTraining")).toBe(114);
|
|
61
|
+
expect(mapSportType("Elliptical")).toBe(25);
|
|
62
|
+
expect(mapSportType("StairStepper")).toBe(78);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns Terra Other (108) for unknown types", () => {
|
|
66
|
+
expect(mapSportType("UnknownSport" as never)).toBe(108);
|
|
67
|
+
expect(mapSportType("" as never)).toBe(108);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ─── Strava SportType → Terra ActivityType ───────────────────────────────────
|
|
2
|
+
// Maps Strava sport_type strings to Terra's ActivityType numeric enum
|
|
3
|
+
// used by the Soma schema.
|
|
4
|
+
//
|
|
5
|
+
// Strava values: https://developers.strava.com/docs/reference/#api-models-SportType
|
|
6
|
+
// Terra values: https://docs.tryterra.co/reference/health-and-fitness-api/data-models#activitytype
|
|
7
|
+
|
|
8
|
+
import type { StravaSportType } from "../types.js";
|
|
9
|
+
|
|
10
|
+
const sportTypeMap: Record<string, number> = {
|
|
11
|
+
// ── Cycling ────────────────────────────────────────────────────────────────
|
|
12
|
+
// Terra Biking = 1
|
|
13
|
+
Ride: 1,
|
|
14
|
+
MountainBikeRide: 1,
|
|
15
|
+
GravelRide: 1,
|
|
16
|
+
EBikeRide: 1,
|
|
17
|
+
EMountainBikeRide: 1,
|
|
18
|
+
VirtualRide: 1,
|
|
19
|
+
Velomobile: 1,
|
|
20
|
+
|
|
21
|
+
// ── Running ────────────────────────────────────────────────────────────────
|
|
22
|
+
// Terra Running = 8
|
|
23
|
+
Run: 8,
|
|
24
|
+
TrailRun: 8,
|
|
25
|
+
VirtualRun: 8,
|
|
26
|
+
|
|
27
|
+
// ── Walking ────────────────────────────────────────────────────────────────
|
|
28
|
+
// Terra Walking = 7
|
|
29
|
+
Walk: 7,
|
|
30
|
+
|
|
31
|
+
// ── Swimming ───────────────────────────────────────────────────────────────
|
|
32
|
+
// Terra Swimming = 82
|
|
33
|
+
Swim: 82,
|
|
34
|
+
|
|
35
|
+
// ── Hiking ─────────────────────────────────────────────────────────────────
|
|
36
|
+
// Terra Hiking = 35
|
|
37
|
+
Hike: 35,
|
|
38
|
+
|
|
39
|
+
// ── Snow Sports ────────────────────────────────────────────────────────────
|
|
40
|
+
AlpineSki: 66, // Terra Alpine Skiing
|
|
41
|
+
BackcountrySki: 66, // Terra Alpine Skiing
|
|
42
|
+
NordicSki: 67, // Terra Cross Country Skiing
|
|
43
|
+
Snowboard: 73, // Terra Snowboarding
|
|
44
|
+
Snowshoe: 74, // Terra Snowshoeing
|
|
45
|
+
|
|
46
|
+
// ── Water Sports ───────────────────────────────────────────────────────────
|
|
47
|
+
Rowing: 53, // Terra Rowing
|
|
48
|
+
VirtualRow: 53, // Terra Rowing
|
|
49
|
+
Kayaking: 40, // Terra Kayaking
|
|
50
|
+
Canoeing: 22, // Terra Canoeing
|
|
51
|
+
Sail: 59, // Terra Sailing
|
|
52
|
+
Surfing: 81, // Terra Surfing
|
|
53
|
+
Kitesurf: 41, // Terra Kitesurfing
|
|
54
|
+
Windsurf: 99, // Terra Windsurfing
|
|
55
|
+
StandUpPaddling: 129, // Terra Paddling
|
|
56
|
+
|
|
57
|
+
// ── Skating ────────────────────────────────────────────────────────────────
|
|
58
|
+
IceSkate: 62, // Terra Skating
|
|
59
|
+
InlineSkate: 62, // Terra Skating
|
|
60
|
+
Skateboard: 62, // Terra Skating
|
|
61
|
+
RollerSki: 62, // Terra Skating
|
|
62
|
+
|
|
63
|
+
// ── Racket Sports ──────────────────────────────────────────────────────────
|
|
64
|
+
Tennis: 87, // Terra Tennis
|
|
65
|
+
TableTennis: 85, // Terra Table Tennis
|
|
66
|
+
Badminton: 10, // Terra Badminton
|
|
67
|
+
Racquetball: 51, // Terra Racquetball
|
|
68
|
+
Squash: 76, // Terra Squash
|
|
69
|
+
Pickleball: 108, // Terra Other (no direct mapping)
|
|
70
|
+
|
|
71
|
+
// ── Gym / Fitness ──────────────────────────────────────────────────────────
|
|
72
|
+
WeightTraining: 80, // Terra Strength Training
|
|
73
|
+
Crossfit: 113, // Terra Crossfit
|
|
74
|
+
Elliptical: 25, // Terra Elliptical
|
|
75
|
+
StairStepper: 78, // Terra Stair Climbing Machine
|
|
76
|
+
Yoga: 100, // Terra Yoga
|
|
77
|
+
Pilates: 49, // Terra Pilates
|
|
78
|
+
HighIntensityIntervalTraining: 114, // Terra HIIT
|
|
79
|
+
|
|
80
|
+
// ── Climbing ───────────────────────────────────────────────────────────────
|
|
81
|
+
RockClimbing: 52, // Terra Rock Climbing
|
|
82
|
+
|
|
83
|
+
// ── Team Sports ────────────────────────────────────────────────────────────
|
|
84
|
+
Soccer: 29, // Terra English Football
|
|
85
|
+
|
|
86
|
+
// ── Other ──────────────────────────────────────────────────────────────────
|
|
87
|
+
Golf: 32, // Terra Golf
|
|
88
|
+
Handcycle: 14, // Terra Handbiking
|
|
89
|
+
Wheelchair: 98, // Terra Wheelchair
|
|
90
|
+
Workout: 108, // Terra Other
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Map a Strava sport_type string to the Terra ActivityType enum.
|
|
95
|
+
* Returns Terra "Other" (108) for unknown types.
|
|
96
|
+
*/
|
|
97
|
+
export function mapSportType(sportType: StravaSportType): number {
|
|
98
|
+
return sportTypeMap[sportType] ?? 108;
|
|
99
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// ─── Strava Sync Helper ──────────────────────────────────────────────────────
|
|
2
|
+
// High-level function that combines the Strava client, transformers,
|
|
3
|
+
// and Soma ingestion into a single call. Designed to be used inside a
|
|
4
|
+
// Convex action.
|
|
5
|
+
|
|
6
|
+
import type { Soma } from "../client/index.js";
|
|
7
|
+
import type { ActionCtx } from "../client/types.js";
|
|
8
|
+
import type { StravaClient } from "./client.js";
|
|
9
|
+
import { transformActivity } from "./activity.js";
|
|
10
|
+
import { transformAthlete } from "./athlete.js";
|
|
11
|
+
|
|
12
|
+
export interface SyncActivitiesOptions {
|
|
13
|
+
/** Authenticated Strava API client. */
|
|
14
|
+
client: StravaClient;
|
|
15
|
+
/** Soma component instance. */
|
|
16
|
+
soma: Soma;
|
|
17
|
+
/** Convex action context (has runMutation for ingestion). */
|
|
18
|
+
ctx: ActionCtx;
|
|
19
|
+
/** The Soma connection ID for this user–Strava link. */
|
|
20
|
+
connectionId: string;
|
|
21
|
+
/** The host app's user identifier (e.g., Clerk user ID). */
|
|
22
|
+
userId: string;
|
|
23
|
+
/**
|
|
24
|
+
* Only sync activities after this Unix epoch timestamp (seconds).
|
|
25
|
+
* Useful for incremental sync — pass the `lastDataUpdate` timestamp.
|
|
26
|
+
*/
|
|
27
|
+
after?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Only sync activities before this Unix epoch timestamp (seconds).
|
|
30
|
+
*/
|
|
31
|
+
before?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Fetch detailed streams (heart rate, power, position, etc.) for each
|
|
34
|
+
* activity. Adds one API call per activity.
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
includeStreams?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Fetch lap data for each activity. Adds one API call per activity
|
|
40
|
+
* (unless the detailed activity already contains laps).
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
includeLaps?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SyncActivitiesResult {
|
|
47
|
+
/** Number of activities successfully synced. */
|
|
48
|
+
synced: number;
|
|
49
|
+
/** Activity IDs that failed to sync (non-fatal). */
|
|
50
|
+
errors: Array<{ activityId: number; error: string }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sync activities from Strava into Soma.
|
|
55
|
+
*
|
|
56
|
+
* This function handles the full flow:
|
|
57
|
+
* 1. Lists activities from the Strava API (with auto-pagination)
|
|
58
|
+
* 2. Optionally fetches detailed data, streams, and laps per activity
|
|
59
|
+
* 3. Transforms each activity into the Soma schema
|
|
60
|
+
* 4. Ingests each activity into Soma (with automatic deduplication)
|
|
61
|
+
*
|
|
62
|
+
* Designed to be called from a Convex action.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { StravaClient, syncActivities } from "@nativesquare/soma/strava";
|
|
67
|
+
* import { Soma } from "@nativesquare/soma";
|
|
68
|
+
*
|
|
69
|
+
* export const syncStrava = internalAction({
|
|
70
|
+
* args: { userId: v.string(), connectionId: v.string(), accessToken: v.string() },
|
|
71
|
+
* handler: async (ctx, { userId, connectionId, accessToken }) => {
|
|
72
|
+
* const client = new StravaClient({ accessToken });
|
|
73
|
+
* const soma = new Soma(components.soma);
|
|
74
|
+
*
|
|
75
|
+
* const result = await syncActivities({
|
|
76
|
+
* client,
|
|
77
|
+
* soma,
|
|
78
|
+
* ctx,
|
|
79
|
+
* connectionId,
|
|
80
|
+
* userId,
|
|
81
|
+
* includeStreams: true,
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* console.log(`Synced ${result.synced} activities`);
|
|
85
|
+
* },
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export async function syncActivities(
|
|
90
|
+
opts: SyncActivitiesOptions,
|
|
91
|
+
): Promise<SyncActivitiesResult> {
|
|
92
|
+
const {
|
|
93
|
+
client,
|
|
94
|
+
soma,
|
|
95
|
+
ctx,
|
|
96
|
+
connectionId,
|
|
97
|
+
userId,
|
|
98
|
+
after,
|
|
99
|
+
before,
|
|
100
|
+
includeStreams = false,
|
|
101
|
+
includeLaps = false,
|
|
102
|
+
} = opts;
|
|
103
|
+
|
|
104
|
+
const summaries = await client.listAllActivities({
|
|
105
|
+
after,
|
|
106
|
+
before,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let synced = 0;
|
|
110
|
+
const errors: SyncActivitiesResult["errors"] = [];
|
|
111
|
+
|
|
112
|
+
for (const summary of summaries) {
|
|
113
|
+
try {
|
|
114
|
+
const detailed = await client.getActivity(summary.id);
|
|
115
|
+
|
|
116
|
+
const streams = includeStreams
|
|
117
|
+
? await client.getActivityStreams(summary.id)
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
const laps =
|
|
121
|
+
includeLaps && (!detailed.laps || detailed.laps.length === 0)
|
|
122
|
+
? await client.getActivityLaps(summary.id)
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
125
|
+
const data = transformActivity(detailed, { streams, laps });
|
|
126
|
+
|
|
127
|
+
await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
128
|
+
synced++;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
errors.push({
|
|
131
|
+
activityId: summary.id,
|
|
132
|
+
error: err instanceof Error ? err.message : String(err),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { synced, errors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface SyncAthleteOptions {
|
|
141
|
+
/** Authenticated Strava API client. */
|
|
142
|
+
client: StravaClient;
|
|
143
|
+
/** Soma component instance. */
|
|
144
|
+
soma: Soma;
|
|
145
|
+
/** Convex action context. */
|
|
146
|
+
ctx: ActionCtx;
|
|
147
|
+
/** The Soma connection ID for this user–Strava link. */
|
|
148
|
+
connectionId: string;
|
|
149
|
+
/** The host app's user identifier. */
|
|
150
|
+
userId: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sync the authenticated athlete's profile from Strava into Soma.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* await syncAthlete({ client, soma, ctx, connectionId, userId });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export async function syncAthlete(
|
|
162
|
+
opts: SyncAthleteOptions,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const { client, soma, ctx, connectionId, userId } = opts;
|
|
165
|
+
const athlete = await client.getAthlete();
|
|
166
|
+
const data = transformAthlete(athlete);
|
|
167
|
+
await soma.ingestAthlete(ctx, { connectionId, userId, ...data });
|
|
168
|
+
}
|