@nativesquare/soma 0.6.0 → 0.7.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 (49) hide show
  1. package/dist/client/index.d.ts +151 -53
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +162 -69
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/component.d.ts +130 -17
  6. package/dist/component/_generated/component.d.ts.map +1 -1
  7. package/dist/component/garmin.d.ts +61 -43
  8. package/dist/component/garmin.d.ts.map +1 -1
  9. package/dist/component/garmin.js +208 -122
  10. package/dist/component/garmin.js.map +1 -1
  11. package/dist/component/public.d.ts +363 -0
  12. package/dist/component/public.d.ts.map +1 -1
  13. package/dist/component/public.js +124 -0
  14. package/dist/component/public.js.map +1 -1
  15. package/dist/component/schema.d.ts +7 -9
  16. package/dist/component/schema.d.ts.map +1 -1
  17. package/dist/component/schema.js +9 -10
  18. package/dist/component/schema.js.map +1 -1
  19. package/dist/component/strava.d.ts +0 -1
  20. package/dist/component/strava.d.ts.map +1 -1
  21. package/dist/component/strava.js +0 -1
  22. package/dist/component/strava.js.map +1 -1
  23. package/dist/component/validators/enums.d.ts +1 -1
  24. package/dist/garmin/auth.d.ts +55 -46
  25. package/dist/garmin/auth.d.ts.map +1 -1
  26. package/dist/garmin/auth.js +82 -122
  27. package/dist/garmin/auth.js.map +1 -1
  28. package/dist/garmin/client.d.ts +64 -17
  29. package/dist/garmin/client.d.ts.map +1 -1
  30. package/dist/garmin/client.js +143 -29
  31. package/dist/garmin/client.js.map +1 -1
  32. package/dist/garmin/index.d.ts +3 -3
  33. package/dist/garmin/index.d.ts.map +1 -1
  34. package/dist/garmin/index.js +4 -4
  35. package/dist/garmin/index.js.map +1 -1
  36. package/dist/garmin/plannedWorkout.d.ts +12 -0
  37. package/dist/garmin/plannedWorkout.d.ts.map +1 -0
  38. package/dist/garmin/plannedWorkout.js +267 -0
  39. package/dist/garmin/plannedWorkout.js.map +1 -0
  40. package/dist/garmin/types.d.ts +78 -6
  41. package/dist/garmin/types.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/client/index.ts +147 -0
  44. package/src/component/_generated/component.ts +142 -0
  45. package/src/component/garmin.ts +118 -0
  46. package/src/component/public.ts +135 -0
  47. package/src/garmin/client.ts +164 -0
  48. package/src/garmin/plannedWorkout.ts +333 -0
  49. package/src/garmin/types.ts +143 -0
@@ -0,0 +1,333 @@
1
+ // ─── Soma → Garmin Training API V2 Transformer ─────────────────────────────
2
+ // Maps Soma's Terra-style planned workout model to the Garmin Training API V2
3
+ // JSON format for workout creation and scheduling.
4
+
5
+ import type {
6
+ GarminWorkout,
7
+ GarminWorkoutSegment,
8
+ GarminWorkoutStep,
9
+ GarminWorkoutRepeatStep,
10
+ GarminWorkoutSport,
11
+ GarminStepIntensity,
12
+ GarminDurationType,
13
+ GarminTargetType,
14
+ } from "./types.js";
15
+
16
+ // ─── Public API ──────────────────────────────────────────────────────────────
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ type SomaPlannedWorkout = Record<string, any>;
20
+
21
+ /**
22
+ * Transform a Soma planned workout document into a Garmin Training API V2
23
+ * workout payload ready for `POST /workoutportal/workout/v2`.
24
+ *
25
+ * @param somaWorkout - The planned workout document from the Soma DB
26
+ * @param providerName - Name shown to the user in Garmin Connect (20 chars max)
27
+ */
28
+ export function transformPlannedWorkoutToGarmin(
29
+ somaWorkout: SomaPlannedWorkout,
30
+ providerName: string,
31
+ ): GarminWorkout {
32
+ const metadata = somaWorkout.metadata ?? {};
33
+ const sport = mapSportType(metadata.type);
34
+ const steps = transformSteps(somaWorkout.steps ?? []);
35
+
36
+ const segment: GarminWorkoutSegment = {
37
+ segmentOrder: 1,
38
+ sport,
39
+ poolLength: metadata.pool_length_meters ?? null,
40
+ poolLengthUnit: metadata.pool_length_meters != null ? "METER" : null,
41
+ estimatedDurationInSecs: null,
42
+ estimatedDistanceInMeters: null,
43
+ steps,
44
+ };
45
+
46
+ return {
47
+ workoutName: metadata.name ?? "Workout",
48
+ description: metadata.description ?? null,
49
+ sport,
50
+ estimatedDurationInSecs: null,
51
+ estimatedDistanceInMeters: null,
52
+ poolLength: metadata.pool_length_meters ?? null,
53
+ poolLengthUnit: metadata.pool_length_meters != null ? "METER" : null,
54
+ workoutProvider: providerName.slice(0, 20),
55
+ workoutSourceId: providerName.slice(0, 20),
56
+ isSessionTransitionEnabled: false,
57
+ segments: [segment],
58
+ };
59
+ }
60
+
61
+ // ─── Step Transformation ─────────────────────────────────────────────────────
62
+
63
+ function transformSteps(
64
+ somaSteps: Array<Record<string, unknown>>,
65
+ ): Array<GarminWorkoutStep | GarminWorkoutRepeatStep> {
66
+ const result: Array<GarminWorkoutStep | GarminWorkoutRepeatStep> = [];
67
+ let order = 1;
68
+
69
+ for (const step of somaSteps) {
70
+ const stepType = String(step.type ?? "STEP").toUpperCase();
71
+
72
+ if (stepType === "REPEAT_STEP") {
73
+ const nestedSteps = Array.isArray(step.steps) ? step.steps : [];
74
+ const reps = extractRepeatCount(step);
75
+
76
+ const repeatStep: GarminWorkoutRepeatStep = {
77
+ type: "WorkoutRepeatStep",
78
+ stepOrder: order++,
79
+ repeatType: "REPEAT_UNTIL_STEPS_CMPLT",
80
+ repeatValue: reps,
81
+ steps: transformSteps(nestedSteps),
82
+ };
83
+ result.push(repeatStep);
84
+ } else {
85
+ const workoutStep = transformSingleStep(step, order++);
86
+ result.push(workoutStep);
87
+ }
88
+ }
89
+
90
+ return result;
91
+ }
92
+
93
+ function transformSingleStep(
94
+ step: Record<string, unknown>,
95
+ stepOrder: number,
96
+ ): GarminWorkoutStep {
97
+ const { durationType, durationValue, durationValueType } =
98
+ extractDuration(step);
99
+ const { targetType, targetValueLow, targetValueHigh, targetValueType } =
100
+ extractTarget(step);
101
+
102
+ return {
103
+ type: "WorkoutStep",
104
+ stepOrder,
105
+ intensity: mapIntensity(step.intensity),
106
+ description: (step.description as string) ?? null,
107
+ durationType,
108
+ durationValue,
109
+ durationValueType,
110
+ targetType,
111
+ targetValue: null,
112
+ targetValueLow,
113
+ targetValueHigh,
114
+ targetValueType,
115
+ secondaryTargetType: null,
116
+ secondaryTargetValue: null,
117
+ secondaryTargetValueLow: null,
118
+ secondaryTargetValueHigh: null,
119
+ secondaryTargetValueType: null,
120
+ strokeType: (step.stroke_type as string) ?? null,
121
+ drillType: null,
122
+ equipmentType: (step.equipment_type as string) ?? null,
123
+ exerciseCategory: (step.exercise_category as string) ?? null,
124
+ exerciseName: (step.exercise_name as string) ?? null,
125
+ weightValue: (step.weight_kg as number) ?? null,
126
+ weightDisplayUnit: step.weight_kg != null ? "KILOGRAM" : null,
127
+ };
128
+ }
129
+
130
+ // ─── Duration Extraction ─────────────────────────────────────────────────────
131
+
132
+ interface DurationResult {
133
+ durationType: GarminDurationType;
134
+ durationValue: number | null;
135
+ durationValueType: string | null;
136
+ }
137
+
138
+ function extractDuration(step: Record<string, unknown>): DurationResult {
139
+ const durations = step.durations as Array<Record<string, unknown>> | undefined;
140
+ if (!durations || durations.length === 0) {
141
+ return { durationType: "OPEN", durationValue: null, durationValueType: null };
142
+ }
143
+
144
+ const dur = durations[0];
145
+ const type = String(dur.duration_type ?? "OPEN").toUpperCase();
146
+
147
+ switch (type) {
148
+ case "TIME":
149
+ return {
150
+ durationType: "TIME",
151
+ durationValue: (dur.seconds as number) ?? null,
152
+ durationValueType: null,
153
+ };
154
+ case "DISTANCE_METERS":
155
+ case "DISTANCE":
156
+ return {
157
+ durationType: "DISTANCE",
158
+ durationValue: (dur.distance_meters as number) ?? null,
159
+ durationValueType: null,
160
+ };
161
+ case "HR_LESS_THAN":
162
+ return {
163
+ durationType: "HR_LESS_THAN",
164
+ durationValue: (dur.hr_below_bpm as number) ?? null,
165
+ durationValueType: null,
166
+ };
167
+ case "HR_GREATER_THAN":
168
+ return {
169
+ durationType: "HR_GREATER_THAN",
170
+ durationValue: (dur.hr_above_bpm as number) ?? null,
171
+ durationValueType: null,
172
+ };
173
+ case "CALORIES":
174
+ return {
175
+ durationType: "CALORIES",
176
+ durationValue: (dur.calories as number) ?? null,
177
+ durationValueType: null,
178
+ };
179
+ case "POWER_LESS_THAN":
180
+ return {
181
+ durationType: "POWER_LESS_THAN",
182
+ durationValue: (dur.power_below_watts as number) ?? null,
183
+ durationValueType: null,
184
+ };
185
+ case "POWER_GREATER_THAN":
186
+ return {
187
+ durationType: "POWER_GREATER_THAN",
188
+ durationValue: (dur.power_above_watts as number) ?? null,
189
+ durationValueType: null,
190
+ };
191
+ case "REPS":
192
+ return {
193
+ durationType: "REPS",
194
+ durationValue: (dur.reps as number) ?? null,
195
+ durationValueType: null,
196
+ };
197
+ case "FIXED_REST":
198
+ return {
199
+ durationType: "FIXED_REST",
200
+ durationValue: (dur.rest_seconds as number) ?? (dur.seconds as number) ?? null,
201
+ durationValueType: null,
202
+ };
203
+ default:
204
+ return { durationType: "OPEN", durationValue: null, durationValueType: null };
205
+ }
206
+ }
207
+
208
+ // ─── Target Extraction ───────────────────────────────────────────────────────
209
+
210
+ interface TargetResult {
211
+ targetType: GarminTargetType | null;
212
+ targetValueLow: number | null;
213
+ targetValueHigh: number | null;
214
+ targetValueType: string | null;
215
+ }
216
+
217
+ function extractTarget(step: Record<string, unknown>): TargetResult {
218
+ const targets = step.targets as Array<Record<string, unknown>> | undefined;
219
+ if (!targets || targets.length === 0) {
220
+ return { targetType: null, targetValueLow: null, targetValueHigh: null, targetValueType: null };
221
+ }
222
+
223
+ const target = targets[0];
224
+ const type = String(target.target_type ?? "OPEN").toUpperCase();
225
+
226
+ switch (type) {
227
+ case "HEART_RATE":
228
+ if (target.hr_percentage_low != null || target.hr_percentage_high != null) {
229
+ return {
230
+ targetType: "HEART_RATE",
231
+ targetValueLow: (target.hr_percentage_low as number) ?? null,
232
+ targetValueHigh: (target.hr_percentage_high as number) ?? null,
233
+ targetValueType: "PERCENT",
234
+ };
235
+ }
236
+ return {
237
+ targetType: "HEART_RATE",
238
+ targetValueLow: (target.hr_bpm_low as number) ?? null,
239
+ targetValueHigh: (target.hr_bpm_high as number) ?? null,
240
+ targetValueType: null,
241
+ };
242
+
243
+ case "SPEED":
244
+ case "PACE":
245
+ return {
246
+ targetType: "SPEED",
247
+ targetValueLow: (target.speed_meters_per_second as number) ?? null,
248
+ targetValueHigh: (target.speed_meters_per_second as number) ?? null,
249
+ targetValueType: null,
250
+ };
251
+
252
+ case "CADENCE":
253
+ return {
254
+ targetType: "CADENCE",
255
+ targetValueLow: (target.cadence_low as number) ?? (target.cadence as number) ?? null,
256
+ targetValueHigh: (target.cadence_high as number) ?? (target.cadence as number) ?? null,
257
+ targetValueType: null,
258
+ };
259
+
260
+ case "POWER":
261
+ if (target.power_percentage_low != null || target.power_percentage_high != null) {
262
+ return {
263
+ targetType: "POWER",
264
+ targetValueLow: (target.power_percentage_low as number) ?? null,
265
+ targetValueHigh: (target.power_percentage_high as number) ?? null,
266
+ targetValueType: "PERCENT",
267
+ };
268
+ }
269
+ return {
270
+ targetType: "POWER",
271
+ targetValueLow: (target.power_watt_low as number) ?? (target.power_watt as number) ?? null,
272
+ targetValueHigh: (target.power_watt_high as number) ?? (target.power_watt as number) ?? null,
273
+ targetValueType: null,
274
+ };
275
+
276
+ case "OPEN":
277
+ return { targetType: "OPEN", targetValueLow: null, targetValueHigh: null, targetValueType: null };
278
+
279
+ default:
280
+ return { targetType: null, targetValueLow: null, targetValueHigh: null, targetValueType: null };
281
+ }
282
+ }
283
+
284
+ // ─── Enum Mapping ────────────────────────────────────────────────────────────
285
+
286
+ const SPORT_MAP: Record<string, GarminWorkoutSport> = {
287
+ RUNNING: "RUNNING",
288
+ BIKING: "CYCLING",
289
+ CYCLING: "CYCLING",
290
+ SWIMMING: "LAP_SWIMMING",
291
+ LAP_SWIMMING: "LAP_SWIMMING",
292
+ STRENGTH_TRAINING: "STRENGTH_TRAINING",
293
+ STRENGTH: "STRENGTH_TRAINING",
294
+ CARDIO: "CARDIO_TRAINING",
295
+ CARDIO_TRAINING: "CARDIO_TRAINING",
296
+ YOGA: "YOGA",
297
+ PILATES: "PILATES",
298
+ MULTI_SPORT: "MULTI_SPORT",
299
+ GENERIC: "GENERIC",
300
+ HIIT: "CARDIO_TRAINING",
301
+ };
302
+
303
+ function mapSportType(type: string | undefined): GarminWorkoutSport {
304
+ if (!type) return "RUNNING";
305
+ return SPORT_MAP[type.toUpperCase()] ?? "GENERIC";
306
+ }
307
+
308
+ const INTENSITY_MAP: Record<string, GarminStepIntensity> = {
309
+ REST: "REST",
310
+ WARMUP: "WARMUP",
311
+ WARM_UP: "WARMUP",
312
+ COOLDOWN: "COOLDOWN",
313
+ COOL_DOWN: "COOLDOWN",
314
+ RECOVERY: "RECOVERY",
315
+ ACTIVE: "ACTIVE",
316
+ INTERVAL: "INTERVAL",
317
+ MAIN: "MAIN",
318
+ };
319
+
320
+ function mapIntensity(value: unknown): GarminStepIntensity {
321
+ if (value == null) return "ACTIVE";
322
+ const str = String(value).toUpperCase();
323
+ return INTENSITY_MAP[str] ?? "ACTIVE";
324
+ }
325
+
326
+ function extractRepeatCount(step: Record<string, unknown>): number {
327
+ const durations = step.durations as Array<Record<string, unknown>> | undefined;
328
+ if (durations && durations.length > 0) {
329
+ const reps = durations[0].reps as number | undefined;
330
+ if (reps != null) return reps;
331
+ }
332
+ return 1;
333
+ }
@@ -325,6 +325,149 @@ export interface GarminOAuth2TokenResponse {
325
325
  refresh_token_expires_in: number;
326
326
  }
327
327
 
328
+ // ─── Training API V2 Types ──────────────────────────────────────────────────
329
+ // Types for the Garmin Connect Training API V2 (workout import/scheduling).
330
+
331
+ export type GarminWorkoutSport =
332
+ | "RUNNING"
333
+ | "CYCLING"
334
+ | "LAP_SWIMMING"
335
+ | "STRENGTH_TRAINING"
336
+ | "CARDIO_TRAINING"
337
+ | "GENERIC"
338
+ | "YOGA"
339
+ | "PILATES"
340
+ | "MULTI_SPORT"
341
+ | (string & {});
342
+
343
+ export type GarminStepIntensity =
344
+ | "REST"
345
+ | "WARMUP"
346
+ | "COOLDOWN"
347
+ | "RECOVERY"
348
+ | "ACTIVE"
349
+ | "INTERVAL"
350
+ | "MAIN"
351
+ | (string & {});
352
+
353
+ export type GarminDurationType =
354
+ | "TIME"
355
+ | "DISTANCE"
356
+ | "HR_LESS_THAN"
357
+ | "HR_GREATER_THAN"
358
+ | "CALORIES"
359
+ | "OPEN"
360
+ | "POWER_LESS_THAN"
361
+ | "POWER_GREATER_THAN"
362
+ | "TIME_AT_VALID_CDA"
363
+ | "FIXED_REST"
364
+ | "REPS"
365
+ | "FIXED_REPETITION"
366
+ | "REPETITION_SWIM_CSS_OFFSET"
367
+ | (string & {});
368
+
369
+ export type GarminTargetType =
370
+ | "SPEED"
371
+ | "HEART_RATE"
372
+ | "CADENCE"
373
+ | "POWER"
374
+ | "GRADE"
375
+ | "RESISTANCE"
376
+ | "POWER_3S"
377
+ | "POWER_10S"
378
+ | "POWER_30S"
379
+ | "POWER_LAP"
380
+ | "SPEED_LAP"
381
+ | "HEART_RATE_LAP"
382
+ | "OPEN"
383
+ | "PACE"
384
+ | (string & {});
385
+
386
+ export type GarminRepeatType =
387
+ | "REPEAT_UNTIL_STEPS_CMPLT"
388
+ | "REPEAT_UNTIL_TIME"
389
+ | "REPEAT_UNTIL_DISTANCE"
390
+ | "REPEAT_UNTIL_CALORIES"
391
+ | "REPEAT_UNTIL_HR_LESS_THAN"
392
+ | "REPEAT_UNTIL_HR_GREATER_THAN"
393
+ | "REPEAT_UNTIL_POWER_LESS_THAN"
394
+ | "REPEAT_UNTIL_POWER_GREATER_THAN"
395
+ | "REPEAT_UNTIL_POWER_LAST_LAP_LESS_THAN"
396
+ | "REPEAT_UNTIL_MAX_POWER_LAST_LAP_LESS_THAN"
397
+ | (string & {});
398
+
399
+ export interface GarminWorkoutStep {
400
+ type: "WorkoutStep";
401
+ stepId?: number;
402
+ stepOrder: number;
403
+ intensity: GarminStepIntensity;
404
+ description?: string | null;
405
+ durationType: GarminDurationType;
406
+ durationValue?: number | null;
407
+ durationValueType?: string | null;
408
+ targetType?: GarminTargetType | null;
409
+ targetValue?: number | null;
410
+ targetValueLow?: number | null;
411
+ targetValueHigh?: number | null;
412
+ targetValueType?: string | null;
413
+ secondaryTargetType?: string | null;
414
+ secondaryTargetValue?: number | null;
415
+ secondaryTargetValueLow?: number | null;
416
+ secondaryTargetValueHigh?: number | null;
417
+ secondaryTargetValueType?: string | null;
418
+ strokeType?: string | null;
419
+ drillType?: string | null;
420
+ equipmentType?: string | null;
421
+ exerciseCategory?: string | null;
422
+ exerciseName?: string | null;
423
+ weightValue?: number | null;
424
+ weightDisplayUnit?: string | null;
425
+ }
426
+
427
+ export interface GarminWorkoutRepeatStep {
428
+ type: "WorkoutRepeatStep";
429
+ stepId?: number;
430
+ stepOrder: number;
431
+ repeatType: GarminRepeatType;
432
+ repeatValue: number;
433
+ skipLastRestStep?: boolean;
434
+ steps: Array<GarminWorkoutStep | GarminWorkoutRepeatStep>;
435
+ }
436
+
437
+ export interface GarminWorkoutSegment {
438
+ segmentOrder: number;
439
+ sport: GarminWorkoutSport;
440
+ poolLength?: number | null;
441
+ poolLengthUnit?: string | null;
442
+ estimatedDurationInSecs?: number | null;
443
+ estimatedDistanceInMeters?: number | null;
444
+ steps: Array<GarminWorkoutStep | GarminWorkoutRepeatStep>;
445
+ }
446
+
447
+ export interface GarminWorkout {
448
+ workoutId?: number;
449
+ ownerId?: number | null;
450
+ workoutName: string;
451
+ description?: string | null;
452
+ updatedDate?: string;
453
+ createdDate?: string;
454
+ sport: GarminWorkoutSport;
455
+ estimatedDurationInSecs?: number | null;
456
+ estimatedDistanceInMeters?: number | null;
457
+ poolLength?: number | null;
458
+ poolLengthUnit?: string | null;
459
+ workoutProvider: string;
460
+ workoutSourceId: string;
461
+ isSessionTransitionEnabled?: boolean;
462
+ segments: GarminWorkoutSegment[];
463
+ }
464
+
465
+ export interface GarminWorkoutSchedule {
466
+ scheduleId?: number;
467
+ workoutId: number;
468
+ date: string;
469
+ }
470
+
328
471
  // ─── API Response Wrappers ──────────────────────────────────────────────────
329
472
 
330
473
  export interface GarminWebhookPayload {