@nativesquare/soma 0.9.3 → 0.10.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.
Files changed (202) hide show
  1. package/dist/client/index.d.ts +96 -33
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +80 -35
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +18 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +43 -9
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/garmin/auth.d.ts +0 -4
  11. package/dist/component/garmin/auth.d.ts.map +1 -1
  12. package/dist/component/garmin/auth.js +0 -8
  13. package/dist/component/garmin/auth.js.map +1 -1
  14. package/dist/component/garmin/private.d.ts +20 -3
  15. package/dist/component/garmin/private.d.ts.map +1 -1
  16. package/dist/component/garmin/private.js +17 -26
  17. package/dist/component/garmin/private.js.map +1 -1
  18. package/dist/component/garmin/public.d.ts +4 -4
  19. package/dist/component/garmin/public.d.ts.map +1 -1
  20. package/dist/component/garmin/public.js +6 -1
  21. package/dist/component/garmin/public.js.map +1 -1
  22. package/dist/component/garmin/webhooks.d.ts +4 -0
  23. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  24. package/dist/component/garmin/webhooks.js +23 -18
  25. package/dist/component/garmin/webhooks.js.map +1 -1
  26. package/dist/component/schema.d.ts +2 -2
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +5 -3
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/{strava → component/strava}/auth.d.ts +15 -48
  31. package/dist/component/strava/auth.d.ts.map +1 -0
  32. package/dist/{strava → component/strava}/auth.js +4 -39
  33. package/dist/component/strava/auth.js.map +1 -0
  34. package/dist/component/strava/client.d.ts +8 -0
  35. package/dist/component/strava/client.d.ts.map +1 -0
  36. package/dist/component/strava/client.js +18 -0
  37. package/dist/component/strava/client.js.map +1 -0
  38. package/dist/component/strava/private.d.ts +19 -0
  39. package/dist/component/strava/private.d.ts.map +1 -1
  40. package/dist/component/strava/private.js +52 -2
  41. package/dist/component/strava/private.js.map +1 -1
  42. package/dist/component/strava/public.d.ts +87 -12
  43. package/dist/component/strava/public.d.ts.map +1 -1
  44. package/dist/component/strava/public.js +218 -92
  45. package/dist/component/strava/public.js.map +1 -1
  46. package/dist/component/strava/transform/activity.d.ts +19 -0
  47. package/dist/component/strava/transform/activity.d.ts.map +1 -0
  48. package/dist/{strava → component/strava/transform}/activity.js +21 -41
  49. package/dist/component/strava/transform/activity.js.map +1 -0
  50. package/dist/{strava → component/strava/transform}/athlete.d.ts +4 -10
  51. package/dist/component/strava/transform/athlete.d.ts.map +1 -0
  52. package/dist/{strava → component/strava/transform}/athlete.js +2 -8
  53. package/dist/component/strava/transform/athlete.js.map +1 -0
  54. package/dist/component/strava/transform/maps/sportType.d.ts +7 -0
  55. package/dist/component/strava/transform/maps/sportType.d.ts.map +1 -0
  56. package/dist/{strava/maps/sport-type.js → component/strava/transform/maps/sportType.js} +4 -2
  57. package/dist/component/strava/transform/maps/sportType.js.map +1 -0
  58. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts +3 -0
  59. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts.map +1 -0
  60. package/dist/component/strava/types/stravaApi/client/client.gen.js +236 -0
  61. package/dist/component/strava/types/stravaApi/client/client.gen.js.map +1 -0
  62. package/dist/component/strava/types/stravaApi/client/index.d.ts +9 -0
  63. package/dist/component/strava/types/stravaApi/client/index.d.ts.map +1 -0
  64. package/dist/component/strava/types/stravaApi/client/index.js +7 -0
  65. package/dist/component/strava/types/stravaApi/client/index.js.map +1 -0
  66. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts +118 -0
  67. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts.map +1 -0
  68. package/dist/component/strava/types/stravaApi/client/types.gen.js +3 -0
  69. package/dist/component/strava/types/stravaApi/client/types.gen.js.map +1 -0
  70. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts +34 -0
  71. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts.map +1 -0
  72. package/dist/component/strava/types/stravaApi/client/utils.gen.js +229 -0
  73. package/dist/component/strava/types/stravaApi/client/utils.gen.js.map +1 -0
  74. package/dist/component/strava/types/stravaApi/client.gen.d.ts +13 -0
  75. package/dist/component/strava/types/stravaApi/client.gen.d.ts.map +1 -0
  76. package/dist/component/strava/types/stravaApi/client.gen.js +4 -0
  77. package/dist/component/strava/types/stravaApi/client.gen.js.map +1 -0
  78. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts +19 -0
  79. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts.map +1 -0
  80. package/dist/component/strava/types/stravaApi/core/auth.gen.js +15 -0
  81. package/dist/component/strava/types/stravaApi/core/auth.gen.js.map +1 -0
  82. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts +26 -0
  83. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts.map +1 -0
  84. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js +58 -0
  85. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js.map +1 -0
  86. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts +44 -0
  87. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts.map +1 -0
  88. package/dist/component/strava/types/stravaApi/core/params.gen.js +101 -0
  89. package/dist/component/strava/types/stravaApi/core/params.gen.js.map +1 -0
  90. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts +34 -0
  91. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts.map +1 -0
  92. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js +107 -0
  93. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js.map +1 -0
  94. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts +19 -0
  95. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts.map +1 -0
  96. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js +93 -0
  97. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js.map +1 -0
  98. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts +72 -0
  99. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts.map +1 -0
  100. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js +134 -0
  101. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js.map +1 -0
  102. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts +79 -0
  103. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts.map +1 -0
  104. package/dist/component/strava/types/stravaApi/core/types.gen.js +3 -0
  105. package/dist/component/strava/types/stravaApi/core/types.gen.js.map +1 -0
  106. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts +20 -0
  107. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts.map +1 -0
  108. package/dist/component/strava/types/stravaApi/core/utils.gen.js +88 -0
  109. package/dist/component/strava/types/stravaApi/core/utils.gen.js.map +1 -0
  110. package/dist/component/strava/types/stravaApi/index.d.ts +3 -0
  111. package/dist/component/strava/types/stravaApi/index.d.ts.map +1 -0
  112. package/dist/component/strava/types/stravaApi/index.js +3 -0
  113. package/dist/component/strava/types/stravaApi/index.js.map +1 -0
  114. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts +224 -0
  115. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts.map +1 -0
  116. package/dist/component/strava/types/stravaApi/sdk.gen.js +361 -0
  117. package/dist/component/strava/types/stravaApi/sdk.gen.js.map +1 -0
  118. package/dist/component/strava/types/stravaApi/types.gen.d.ts +2209 -0
  119. package/dist/component/strava/types/stravaApi/types.gen.d.ts.map +1 -0
  120. package/dist/component/strava/types/stravaApi/types.gen.js +3 -0
  121. package/dist/component/strava/types/stravaApi/types.gen.js.map +1 -0
  122. package/dist/component/strava/types/stravaApi/zod.gen.d.ts +5332 -0
  123. package/dist/component/strava/types/stravaApi/zod.gen.d.ts.map +1 -0
  124. package/dist/component/strava/types/stravaApi/zod.gen.js +1009 -0
  125. package/dist/component/strava/types/stravaApi/zod.gen.js.map +1 -0
  126. package/dist/component/strava/utils.d.ts +15 -0
  127. package/dist/component/strava/utils.d.ts.map +1 -0
  128. package/dist/component/strava/utils.js +36 -0
  129. package/dist/component/strava/utils.js.map +1 -0
  130. package/dist/component/utils.d.ts +5 -0
  131. package/dist/component/utils.d.ts.map +1 -0
  132. package/dist/component/utils.js +11 -0
  133. package/dist/component/utils.js.map +1 -0
  134. package/package.json +131 -130
  135. package/src/client/index.ts +121 -52
  136. package/src/component/_generated/api.ts +18 -0
  137. package/src/component/_generated/component.ts +44 -11
  138. package/src/component/garmin/auth.ts +0 -9
  139. package/src/component/garmin/private.ts +0 -12
  140. package/src/component/garmin/public.ts +8 -1
  141. package/src/component/schema.ts +5 -3
  142. package/src/{strava → component/strava}/auth.ts +143 -185
  143. package/src/component/strava/client.ts +20 -0
  144. package/src/component/strava/private.ts +147 -89
  145. package/src/component/strava/public.ts +268 -110
  146. package/src/{strava → component/strava/transform}/activity.ts +256 -276
  147. package/src/{strava → component/strava/transform}/athlete.ts +41 -47
  148. package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
  149. package/src/component/strava/types/specs/strava-api.json +4796 -0
  150. package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
  151. package/src/component/strava/types/stravaApi/client/index.ts +25 -0
  152. package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
  153. package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
  154. package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
  155. package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
  156. package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
  157. package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
  158. package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
  159. package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
  160. package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
  161. package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
  162. package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
  163. package/src/component/strava/types/stravaApi/index.ts +4 -0
  164. package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
  165. package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
  166. package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
  167. package/src/component/strava/utils.ts +52 -0
  168. package/src/component/utils.ts +11 -0
  169. package/dist/strava/activity.d.ts +0 -121
  170. package/dist/strava/activity.d.ts.map +0 -1
  171. package/dist/strava/activity.js.map +0 -1
  172. package/dist/strava/athlete.d.ts.map +0 -1
  173. package/dist/strava/athlete.js.map +0 -1
  174. package/dist/strava/auth.d.ts.map +0 -1
  175. package/dist/strava/auth.js.map +0 -1
  176. package/dist/strava/client.d.ts +0 -93
  177. package/dist/strava/client.d.ts.map +0 -1
  178. package/dist/strava/client.js +0 -158
  179. package/dist/strava/client.js.map +0 -1
  180. package/dist/strava/index.d.ts +0 -13
  181. package/dist/strava/index.d.ts.map +0 -1
  182. package/dist/strava/index.js +0 -17
  183. package/dist/strava/index.js.map +0 -1
  184. package/dist/strava/maps/sport-type.d.ts +0 -7
  185. package/dist/strava/maps/sport-type.d.ts.map +0 -1
  186. package/dist/strava/maps/sport-type.js.map +0 -1
  187. package/dist/strava/sync.d.ts +0 -104
  188. package/dist/strava/sync.d.ts.map +0 -1
  189. package/dist/strava/sync.js +0 -87
  190. package/dist/strava/sync.js.map +0 -1
  191. package/dist/strava/types.d.ts +0 -266
  192. package/dist/strava/types.d.ts.map +0 -1
  193. package/dist/strava/types.js +0 -8
  194. package/dist/strava/types.js.map +0 -1
  195. package/src/strava/activity.test.ts +0 -415
  196. package/src/strava/athlete.test.ts +0 -139
  197. package/src/strava/auth.test.ts +0 -78
  198. package/src/strava/client.ts +0 -212
  199. package/src/strava/index.ts +0 -54
  200. package/src/strava/maps/sport-type.test.ts +0 -69
  201. package/src/strava/sync.ts +0 -168
  202. package/src/strava/types.ts +0 -361
@@ -1,276 +1,256 @@
1
- // ─── Activity Transformer ────────────────────────────────────────────────────
2
- // Transforms a Strava DetailedActivity (+ optional streams/laps) into the
3
- // Soma Activity schema shape.
4
-
5
- import type { DetailedActivity, SummaryActivity, StreamSet, Lap } from "./types.js";
6
- import { mapSportType } from "./maps/sport-type.js";
7
-
8
- /**
9
- * The output shape of {@link transformActivity}, matching the Soma Activity
10
- * validator minus `connectionId` and `userId` (added at ingestion time).
11
- */
12
- export type ActivityData = ReturnType<typeof transformActivity>;
13
-
14
- /**
15
- * Transform a Strava activity into a Soma Activity document shape.
16
- *
17
- * The returned object is ready to be spread into an `ingestActivity` call
18
- * alongside `connectionId` and `userId`.
19
- *
20
- * Accepts either a DetailedActivity (from `GET /activities/{id}`) or a
21
- * SummaryActivity (from `GET /athlete/activities`). When a DetailedActivity
22
- * is provided, additional fields like `calories`, `segment_efforts`, and
23
- * embedded `laps` are mapped. Optional streams and laps can also be supplied
24
- * for time-series data (heart rate, power, position, etc.).
25
- *
26
- * @param activity - The Strava activity (summary or detailed)
27
- * @param opts - Optional streams and laps data
28
- * @returns Soma Activity fields (without connectionId/userId)
29
- *
30
- * @example
31
- * ```ts
32
- * const data = transformActivity(stravaActivity, { streams, laps });
33
- * await soma.ingestActivity(ctx, { connectionId, userId, ...data });
34
- * ```
35
- */
36
- export function transformActivity(
37
- activity: DetailedActivity | SummaryActivity,
38
- opts?: { streams?: StreamSet; laps?: Lap[] },
39
- ) {
40
- const streams = opts?.streams;
41
- const laps = opts?.laps ?? (isDetailed(activity) ? activity.laps : undefined);
42
- const timeStream = streams?.time?.data;
43
- const startDate = activity.start_date;
44
-
45
- return {
46
- metadata: {
47
- summary_id: String(activity.id),
48
- start_time: activity.start_date,
49
- end_time: computeEndTime(activity.start_date, activity.elapsed_time),
50
- type: mapSportType(activity.sport_type),
51
- upload_type: activity.manual ? 2 : 1, // 2=Manual, 1=Automatic
52
- name: activity.name,
53
- city: activity.location_city ?? undefined,
54
- state: activity.location_state ?? undefined,
55
- country: activity.location_country ?? undefined,
56
- },
57
-
58
- active_durations_data: {
59
- activity_seconds: activity.moving_time,
60
- },
61
-
62
- calories_data: isDetailed(activity) && activity.calories != null
63
- ? { total_burned_calories: activity.calories }
64
- : undefined,
65
-
66
- device_data: activity.device_name
67
- ? { name: activity.device_name }
68
- : undefined,
69
-
70
- distance_data: buildDistanceData(activity),
71
-
72
- energy_data: activity.kilojoules != null
73
- ? { energy_kilojoules: activity.kilojoules }
74
- : undefined,
75
-
76
- heart_rate_data: buildHeartRateData(activity, streams, timeStream, startDate),
77
-
78
- lap_data: buildLapData(laps),
79
-
80
- movement_data: buildMovementData(activity, streams, timeStream, startDate),
81
-
82
- polyline_map_data: activity.map?.summary_polyline
83
- ? { summary_polyline: activity.map.summary_polyline }
84
- : undefined,
85
-
86
- position_data: buildPositionData(activity, streams, timeStream, startDate),
87
-
88
- power_data: buildPowerData(activity, streams, timeStream, startDate),
89
- };
90
- }
91
-
92
- // ─── Helpers ─────────────────────────────────────────────────────────────────
93
-
94
- function isDetailed(
95
- activity: DetailedActivity | SummaryActivity,
96
- ): activity is DetailedActivity {
97
- return activity.resource_state === 3;
98
- }
99
-
100
- function computeEndTime(startDate: string, elapsedTimeSeconds: number): string {
101
- const start = new Date(startDate);
102
- return new Date(start.getTime() + elapsedTimeSeconds * 1000).toISOString();
103
- }
104
-
105
- /**
106
- * Compute an ISO-8601 timestamp for a stream data point, given the
107
- * activity start time and the time stream offset in seconds.
108
- */
109
- function streamTimestamp(
110
- startDate: string,
111
- timeOffsetSeconds: number,
112
- ): string {
113
- const start = new Date(startDate);
114
- return new Date(start.getTime() + timeOffsetSeconds * 1000).toISOString();
115
- }
116
-
117
- function buildDistanceData(activity: DetailedActivity | SummaryActivity) {
118
- if (activity.distance == null && activity.total_elevation_gain == null) {
119
- return undefined;
120
- }
121
-
122
- const detailed = isDetailed(activity) ? activity : undefined;
123
-
124
- return {
125
- summary: {
126
- distance_meters: activity.distance ?? undefined,
127
- elevation:
128
- activity.total_elevation_gain != null
129
- ? {
130
- gain_actual_meters: activity.total_elevation_gain,
131
- max_meters: detailed?.elev_high ?? undefined,
132
- min_meters: detailed?.elev_low ?? undefined,
133
- }
134
- : undefined,
135
- },
136
- };
137
- }
138
-
139
- function buildHeartRateData(
140
- activity: DetailedActivity | SummaryActivity,
141
- streams: StreamSet | undefined,
142
- timeStream: number[] | undefined,
143
- startDate: string,
144
- ) {
145
- const hasHrSummary =
146
- activity.average_heartrate != null || activity.max_heartrate != null;
147
- const hrStream = streams?.heartrate?.data;
148
- const hasHrStream = hrStream && hrStream.length > 0 && timeStream;
149
-
150
- if (!hasHrSummary && !hasHrStream) return undefined;
151
-
152
- return {
153
- summary: hasHrSummary
154
- ? {
155
- avg_hr_bpm: activity.average_heartrate,
156
- max_hr_bpm: activity.max_heartrate,
157
- }
158
- : undefined,
159
- detailed:
160
- hasHrStream && timeStream
161
- ? {
162
- hr_samples: hrStream.map((bpm, i) => ({
163
- timestamp: streamTimestamp(startDate, timeStream[i]),
164
- bpm,
165
- })),
166
- }
167
- : undefined,
168
- };
169
- }
170
-
171
- function buildLapData(laps: Lap[] | undefined) {
172
- if (!laps || laps.length === 0) return undefined;
173
-
174
- return {
175
- laps: laps.map((lap) => ({
176
- start_time: lap.start_date,
177
- end_time: computeEndTime(lap.start_date, lap.elapsed_time),
178
- distance_meters: lap.distance,
179
- calories: undefined as number | undefined,
180
- avg_speed_meters_per_second: lap.average_speed,
181
- avg_hr_bpm: lap.average_heartrate,
182
- })),
183
- };
184
- }
185
-
186
- function buildMovementData(
187
- activity: DetailedActivity | SummaryActivity,
188
- streams: StreamSet | undefined,
189
- timeStream: number[] | undefined,
190
- startDate: string,
191
- ) {
192
- const hasMovement =
193
- activity.average_speed != null ||
194
- activity.max_speed != null ||
195
- activity.average_cadence != null;
196
- const speedStream = streams?.velocity_smooth?.data;
197
- const cadenceStream = streams?.cadence?.data;
198
- const hasStreams =
199
- ((speedStream && speedStream.length > 0) ||
200
- (cadenceStream && cadenceStream.length > 0)) &&
201
- timeStream;
202
-
203
- if (!hasMovement && !hasStreams) return undefined;
204
-
205
- return {
206
- avg_speed_meters_per_second: activity.average_speed ?? undefined,
207
- max_speed_meters_per_second: activity.max_speed ?? undefined,
208
- avg_cadence_rpm: activity.average_cadence ?? undefined,
209
- speed_samples:
210
- speedStream && timeStream
211
- ? speedStream.map((speed, i) => ({
212
- timestamp: streamTimestamp(startDate, timeStream[i]),
213
- speed_meters_per_second: speed,
214
- }))
215
- : undefined,
216
- cadence_samples:
217
- cadenceStream && timeStream
218
- ? cadenceStream.map((cadence, i) => ({
219
- timestamp: streamTimestamp(startDate, timeStream[i]),
220
- cadence_rpm: cadence,
221
- }))
222
- : undefined,
223
- };
224
- }
225
-
226
- function buildPositionData(
227
- activity: DetailedActivity | SummaryActivity,
228
- streams: StreamSet | undefined,
229
- timeStream: number[] | undefined,
230
- startDate: string,
231
- ) {
232
- const latlngStream = streams?.latlng?.data;
233
- const hasPositionStream = latlngStream && latlngStream.length > 0 && timeStream;
234
- const hasStartEnd =
235
- activity.start_latlng != null || activity.end_latlng != null;
236
-
237
- if (!hasPositionStream && !hasStartEnd) return undefined;
238
-
239
- return {
240
- start_pos_lat_lng_deg: activity.start_latlng ?? undefined,
241
- end_pos_lat_lng_deg: activity.end_latlng ?? undefined,
242
- position_samples:
243
- hasPositionStream && timeStream
244
- ? latlngStream.map((coords, i) => ({
245
- timestamp: streamTimestamp(startDate, timeStream[i]),
246
- coords_lat_lng_deg: coords,
247
- }))
248
- : undefined,
249
- };
250
- }
251
-
252
- function buildPowerData(
253
- activity: DetailedActivity | SummaryActivity,
254
- streams: StreamSet | undefined,
255
- timeStream: number[] | undefined,
256
- startDate: string,
257
- ) {
258
- const hasPowerSummary =
259
- activity.average_watts != null || activity.max_watts != null;
260
- const wattsStream = streams?.watts?.data;
261
- const hasWattsStream = wattsStream && wattsStream.length > 0 && timeStream;
262
-
263
- if (!hasPowerSummary && !hasWattsStream) return undefined;
264
-
265
- return {
266
- avg_watts: activity.average_watts ?? undefined,
267
- max_watts: activity.max_watts ?? undefined,
268
- power_samples:
269
- hasWattsStream && timeStream
270
- ? wattsStream.map((watts, i) => ({
271
- timestamp: streamTimestamp(startDate, timeStream[i]),
272
- watts,
273
- }))
274
- : undefined,
275
- };
276
- }
1
+ // ─── Activity Transformer ────────────────────────────────────────────────────
2
+ // Transforms a Strava DetailedActivity (+ optional streams/laps) into the
3
+ // Soma Activity schema shape.
4
+
5
+ import type { DetailedActivity, SummaryActivity, StreamSet, Lap } from "../types/stravaApi/types.gen.js";
6
+ import type { SomaActivity } from "../../validators/activity.js";
7
+ import { mapSportType } from "./maps/sportType.js";
8
+
9
+ /**
10
+ * Transform a Strava activity into a Soma Activity document shape.
11
+ *
12
+ * The returned object is ready to be spread into an `ingestActivity` call
13
+ * alongside `connectionId` and `userId`.
14
+ *
15
+ * Accepts either a DetailedActivity (from `GET /activities/{id}`) or a
16
+ * SummaryActivity (from `GET /athlete/activities`). When a DetailedActivity
17
+ * is provided, additional fields like `calories`, `segment_efforts`, and
18
+ * embedded `laps` are mapped. Optional streams and laps can also be supplied
19
+ * for time-series data (heart rate, power, position, etc.).
20
+ */
21
+ export function transformActivity(
22
+ activity: DetailedActivity | SummaryActivity,
23
+ opts?: { streams?: StreamSet; laps?: Lap[] },
24
+ ): SomaActivity {
25
+ if (activity.id == null || !activity.start_date || activity.elapsed_time == null) {
26
+ throw new Error(
27
+ `Strava activity missing required fields (id=${activity.id}, start_date=${activity.start_date}, elapsed_time=${activity.elapsed_time})`,
28
+ );
29
+ }
30
+
31
+ const streams = opts?.streams;
32
+ const laps = opts?.laps ?? (isDetailed(activity) ? activity.laps : undefined);
33
+ const timeStream = streams?.time?.data;
34
+ const startDate = activity.start_date;
35
+
36
+ return {
37
+ metadata: {
38
+ summary_id: String(activity.id),
39
+ start_time: activity.start_date,
40
+ end_time: computeEndTime(activity.start_date, activity.elapsed_time),
41
+ type: mapSportType(activity.sport_type),
42
+ upload_type: activity.manual ? 2 : 1, // 2=Manual, 1=Automatic
43
+ name: activity.name ?? undefined,
44
+ },
45
+
46
+ active_durations_data: activity.moving_time != null
47
+ ? { activity_seconds: activity.moving_time }
48
+ : undefined,
49
+
50
+ calories_data: isDetailed(activity) && activity.calories != null
51
+ ? { total_burned_calories: activity.calories }
52
+ : undefined,
53
+
54
+ device_data: activity.device_name
55
+ ? { name: activity.device_name }
56
+ : undefined,
57
+
58
+ distance_data: buildDistanceData(activity),
59
+
60
+ energy_data: activity.kilojoules != null
61
+ ? { energy_kilojoules: activity.kilojoules }
62
+ : undefined,
63
+
64
+ heart_rate_data: buildHeartRateData(streams, timeStream, startDate),
65
+
66
+ lap_data: buildLapData(laps),
67
+
68
+ movement_data: buildMovementData(activity, streams, timeStream, startDate),
69
+
70
+ polyline_map_data: activity.map?.summary_polyline
71
+ ? { summary_polyline: activity.map.summary_polyline }
72
+ : undefined,
73
+
74
+ position_data: buildPositionData(activity, streams, timeStream, startDate),
75
+
76
+ power_data: buildPowerData(activity, streams, timeStream, startDate),
77
+ };
78
+ }
79
+
80
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
81
+
82
+ function isDetailed(
83
+ activity: DetailedActivity | SummaryActivity,
84
+ ): activity is DetailedActivity {
85
+ return "description" in activity;
86
+ }
87
+
88
+ function computeEndTime(startDate: string, elapsedTimeSeconds: number): string {
89
+ const start = new Date(startDate);
90
+ return new Date(start.getTime() + elapsedTimeSeconds * 1000).toISOString();
91
+ }
92
+
93
+ /**
94
+ * Compute an ISO-8601 timestamp for a stream data point, given the
95
+ * activity start time and the time stream offset in seconds.
96
+ */
97
+ function streamTimestamp(
98
+ startDate: string,
99
+ timeOffsetSeconds: number,
100
+ ): string {
101
+ const start = new Date(startDate);
102
+ return new Date(start.getTime() + timeOffsetSeconds * 1000).toISOString();
103
+ }
104
+
105
+ function buildDistanceData(activity: DetailedActivity | SummaryActivity) {
106
+ if (activity.distance == null && activity.total_elevation_gain == null) {
107
+ return undefined;
108
+ }
109
+
110
+ return {
111
+ summary: {
112
+ distance_meters: activity.distance ?? undefined,
113
+ elevation:
114
+ activity.total_elevation_gain != null
115
+ ? {
116
+ gain_actual_meters: activity.total_elevation_gain,
117
+ max_meters: activity.elev_high ?? undefined,
118
+ min_meters: activity.elev_low ?? undefined,
119
+ }
120
+ : undefined,
121
+ },
122
+ };
123
+ }
124
+
125
+ function buildHeartRateData(
126
+ streams: StreamSet | undefined,
127
+ timeStream: number[] | undefined,
128
+ startDate: string,
129
+ ) {
130
+ const hrStream = streams?.heartrate?.data;
131
+ const hasHrStream = hrStream && hrStream.length > 0 && timeStream;
132
+
133
+ if (!hasHrStream) return undefined;
134
+
135
+ return {
136
+ detailed:
137
+ timeStream
138
+ ? {
139
+ hr_samples: hrStream.map((bpm, i) => ({
140
+ timestamp: streamTimestamp(startDate, timeStream[i]),
141
+ bpm,
142
+ })),
143
+ }
144
+ : undefined,
145
+ };
146
+ }
147
+
148
+ function buildLapData(laps: Lap[] | undefined) {
149
+ if (!laps || laps.length === 0) return undefined;
150
+
151
+ return {
152
+ laps: laps.flatMap((lap) => {
153
+ const startDate = lap.start_date;
154
+ const elapsedTime = lap.elapsed_time;
155
+ if (!startDate || elapsedTime == null) return [];
156
+ return [
157
+ {
158
+ start_time: startDate,
159
+ end_time: computeEndTime(startDate, elapsedTime),
160
+ distance_meters: lap.distance ?? undefined,
161
+ calories: undefined as number | undefined,
162
+ avg_speed_meters_per_second: lap.average_speed ?? undefined,
163
+ },
164
+ ];
165
+ }),
166
+ };
167
+ }
168
+
169
+ function buildMovementData(
170
+ activity: DetailedActivity | SummaryActivity,
171
+ streams: StreamSet | undefined,
172
+ timeStream: number[] | undefined,
173
+ startDate: string,
174
+ ) {
175
+ const hasMovement =
176
+ activity.average_speed != null || activity.max_speed != null;
177
+ const speedStream = streams?.velocity_smooth?.data;
178
+ const cadenceStream = streams?.cadence?.data;
179
+ const hasStreams =
180
+ ((speedStream && speedStream.length > 0) ||
181
+ (cadenceStream && cadenceStream.length > 0)) &&
182
+ timeStream;
183
+
184
+ if (!hasMovement && !hasStreams) return undefined;
185
+
186
+ return {
187
+ avg_speed_meters_per_second: activity.average_speed ?? undefined,
188
+ max_speed_meters_per_second: activity.max_speed ?? undefined,
189
+ speed_samples:
190
+ speedStream && timeStream
191
+ ? speedStream.map((speed, i) => ({
192
+ timestamp: streamTimestamp(startDate, timeStream[i]),
193
+ speed_meters_per_second: speed,
194
+ }))
195
+ : undefined,
196
+ cadence_samples:
197
+ cadenceStream && timeStream
198
+ ? cadenceStream.map((cadence, i) => ({
199
+ timestamp: streamTimestamp(startDate, timeStream[i]),
200
+ cadence_rpm: cadence,
201
+ }))
202
+ : undefined,
203
+ };
204
+ }
205
+
206
+ function buildPositionData(
207
+ activity: DetailedActivity | SummaryActivity,
208
+ streams: StreamSet | undefined,
209
+ timeStream: number[] | undefined,
210
+ startDate: string,
211
+ ) {
212
+ const latlngStream = streams?.latlng?.data;
213
+ const hasPositionStream = latlngStream && latlngStream.length > 0 && timeStream;
214
+ const hasStartEnd =
215
+ activity.start_latlng != null || activity.end_latlng != null;
216
+
217
+ if (!hasPositionStream && !hasStartEnd) return undefined;
218
+
219
+ return {
220
+ start_pos_lat_lng_deg: activity.start_latlng ?? undefined,
221
+ end_pos_lat_lng_deg: activity.end_latlng ?? undefined,
222
+ position_samples:
223
+ hasPositionStream && timeStream
224
+ ? latlngStream.map((coords, i) => ({
225
+ timestamp: streamTimestamp(startDate, timeStream[i]),
226
+ coords_lat_lng_deg: coords,
227
+ }))
228
+ : undefined,
229
+ };
230
+ }
231
+
232
+ function buildPowerData(
233
+ activity: DetailedActivity | SummaryActivity,
234
+ streams: StreamSet | undefined,
235
+ timeStream: number[] | undefined,
236
+ startDate: string,
237
+ ) {
238
+ const hasPowerSummary =
239
+ activity.average_watts != null || activity.max_watts != null;
240
+ const wattsStream = streams?.watts?.data;
241
+ const hasWattsStream = wattsStream && wattsStream.length > 0 && timeStream;
242
+
243
+ if (!hasPowerSummary && !hasWattsStream) return undefined;
244
+
245
+ return {
246
+ avg_watts: activity.average_watts ?? undefined,
247
+ max_watts: activity.max_watts ?? undefined,
248
+ power_samples:
249
+ hasWattsStream && timeStream
250
+ ? wattsStream.map((watts, i) => ({
251
+ timestamp: streamTimestamp(startDate, timeStream[i]),
252
+ watts,
253
+ }))
254
+ : undefined,
255
+ };
256
+ }
@@ -1,47 +1,41 @@
1
- // ─── Athlete Transformer ─────────────────────────────────────────────────────
2
- // Transforms a Strava DetailedAthlete into the Soma Athlete schema shape.
3
-
4
- import type { DetailedAthlete } from "./types.js";
5
-
6
- /**
7
- * The output shape of {@link transformAthlete}.
8
- */
9
- export type AthleteData = ReturnType<typeof transformAthlete>;
10
-
11
- /**
12
- * Transform a Strava athlete profile into a Soma Athlete document shape.
13
- *
14
- * Strava provides a relatively rich profile compared to HealthKit, including
15
- * name, location, sex, and the date the athlete joined Strava.
16
- *
17
- * @param athlete - The Strava DetailedAthlete from `GET /athlete`
18
- * @returns Soma Athlete fields (without connectionId/userId)
19
- *
20
- * @example
21
- * ```ts
22
- * const data = transformAthlete(stravaAthlete);
23
- * await soma.ingestAthlete(ctx, { connectionId, userId, ...data });
24
- * ```
25
- */
26
- export function transformAthlete(athlete: DetailedAthlete) {
27
- const sexMap: Record<string, string> = {
28
- M: "male",
29
- F: "female",
30
- };
31
-
32
- return {
33
- first_name: athlete.firstname ?? undefined,
34
- last_name: athlete.lastname ?? undefined,
35
- city: athlete.city ?? undefined,
36
- state: athlete.state ?? undefined,
37
- country: athlete.country ?? undefined,
38
- sex: athlete.sex ? sexMap[athlete.sex] : undefined,
39
- joined_provider: athlete.created_at ?? undefined,
40
- devices: athlete.bikes && athlete.shoes
41
- ? [
42
- ...athlete.bikes.map((b) => ({ name: b.name, id: b.id })),
43
- ...athlete.shoes.map((s) => ({ name: s.name, id: s.id })),
44
- ]
45
- : undefined,
46
- };
47
- }
1
+ // ─── Athlete Transformer ─────────────────────────────────────────────────────
2
+ // Transforms a Strava DetailedAthlete into the Soma Athlete schema shape.
3
+
4
+ import type { DetailedAthlete } from "../types/stravaApi/types.gen.js";
5
+
6
+ /**
7
+ * The output shape of {@link transformAthlete}.
8
+ */
9
+ export type AthleteData = ReturnType<typeof transformAthlete>;
10
+
11
+ /**
12
+ * Transform a Strava athlete profile into a Soma Athlete document shape.
13
+ *
14
+ * Strava provides a relatively rich profile compared to HealthKit, including
15
+ * name, location, sex, and the date the athlete joined Strava.
16
+ *
17
+ * @param athlete - The Strava DetailedAthlete from `GET /athlete`
18
+ * @returns Soma Athlete fields (without connectionId/userId)
19
+ */
20
+ export function transformAthlete(athlete: DetailedAthlete) {
21
+ const sexMap: Record<string, string> = {
22
+ M: "male",
23
+ F: "female",
24
+ };
25
+
26
+ return {
27
+ first_name: athlete.firstname ?? undefined,
28
+ last_name: athlete.lastname ?? undefined,
29
+ city: athlete.city ?? undefined,
30
+ state: athlete.state ?? undefined,
31
+ country: athlete.country ?? undefined,
32
+ sex: athlete.sex ? sexMap[athlete.sex] : undefined,
33
+ joined_provider: athlete.created_at ?? undefined,
34
+ devices: athlete.bikes && athlete.shoes
35
+ ? [
36
+ ...athlete.bikes.map((b) => ({ name: b.name ?? "", id: b.id ?? "" })),
37
+ ...athlete.shoes.map((s) => ({ name: s.name ?? "", id: s.id ?? "" })),
38
+ ]
39
+ : undefined,
40
+ };
41
+ }