@nativesquare/soma 0.14.0 → 0.16.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 (164) hide show
  1. package/dist/client/garmin.d.ts +31 -0
  2. package/dist/client/garmin.d.ts.map +1 -1
  3. package/dist/client/garmin.js +34 -0
  4. package/dist/client/garmin.js.map +1 -1
  5. package/dist/client/healthkit.d.ts +267 -0
  6. package/dist/client/healthkit.d.ts.map +1 -0
  7. package/dist/client/healthkit.js +600 -0
  8. package/dist/client/healthkit.js.map +1 -0
  9. package/dist/client/index.d.ts +4 -1
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +4 -0
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/client/types.d.ts +3 -2
  14. package/dist/client/types.d.ts.map +1 -1
  15. package/dist/component/_generated/api.d.ts +26 -0
  16. package/dist/component/_generated/api.d.ts.map +1 -1
  17. package/dist/component/_generated/api.js.map +1 -1
  18. package/dist/component/_generated/component.d.ts +7 -0
  19. package/dist/component/_generated/component.d.ts.map +1 -1
  20. package/dist/component/garmin/private.d.ts +18 -85
  21. package/dist/component/garmin/private.d.ts.map +1 -1
  22. package/dist/component/garmin/private.js +12 -12
  23. package/dist/component/garmin/private.js.map +1 -1
  24. package/dist/component/garmin/public.d.ts +38 -65
  25. package/dist/component/garmin/public.d.ts.map +1 -1
  26. package/dist/component/garmin/public.js +132 -10
  27. package/dist/component/garmin/public.js.map +1 -1
  28. package/dist/component/garmin/utils.d.ts +48 -0
  29. package/dist/component/garmin/utils.d.ts.map +1 -1
  30. package/dist/component/garmin/utils.js +65 -0
  31. package/dist/component/garmin/utils.js.map +1 -1
  32. package/dist/component/garmin/webhooks.d.ts +1 -11
  33. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  34. package/dist/component/garmin/webhooks.js.map +1 -1
  35. package/dist/component/healthkit/index.d.ts +14 -0
  36. package/dist/component/healthkit/index.d.ts.map +1 -0
  37. package/dist/{healthkit → component/healthkit}/index.js +11 -11
  38. package/dist/component/healthkit/index.js.map +1 -0
  39. package/dist/component/healthkit/transform/activity.d.ts +19 -0
  40. package/dist/component/healthkit/transform/activity.d.ts.map +1 -0
  41. package/dist/{healthkit → component/healthkit/transform}/activity.js +1 -1
  42. package/dist/component/healthkit/transform/activity.js.map +1 -0
  43. package/dist/{healthkit → component/healthkit/transform}/athlete.d.ts +3 -9
  44. package/dist/component/healthkit/transform/athlete.d.ts.map +1 -0
  45. package/dist/component/healthkit/transform/athlete.js.map +1 -0
  46. package/dist/component/healthkit/transform/body.d.ts +25 -0
  47. package/dist/component/healthkit/transform/body.d.ts.map +1 -0
  48. package/dist/component/healthkit/transform/body.js.map +1 -0
  49. package/dist/component/healthkit/transform/daily.d.ts +36 -0
  50. package/dist/component/healthkit/transform/daily.d.ts.map +1 -0
  51. package/dist/component/healthkit/transform/daily.js.map +1 -0
  52. package/dist/{healthkit/maps/activity-type.d.ts → component/healthkit/transform/maps/activityType.d.ts} +1 -1
  53. package/dist/component/healthkit/transform/maps/activityType.d.ts.map +1 -0
  54. package/dist/{healthkit/maps/activity-type.js → component/healthkit/transform/maps/activityType.js} +1 -1
  55. package/dist/component/healthkit/transform/maps/activityType.js.map +1 -0
  56. package/dist/{healthkit/maps/menstruation-flow.d.ts → component/healthkit/transform/maps/menstruationFlow.d.ts} +1 -1
  57. package/dist/component/healthkit/transform/maps/menstruationFlow.d.ts.map +1 -0
  58. package/dist/{healthkit/maps/menstruation-flow.js → component/healthkit/transform/maps/menstruationFlow.js} +2 -2
  59. package/dist/component/healthkit/transform/maps/menstruationFlow.js.map +1 -0
  60. package/dist/{healthkit/maps/sleep-level.d.ts → component/healthkit/transform/maps/sleepLevel.d.ts} +1 -1
  61. package/dist/component/healthkit/transform/maps/sleepLevel.d.ts.map +1 -0
  62. package/dist/{healthkit/maps/sleep-level.js → component/healthkit/transform/maps/sleepLevel.js} +2 -2
  63. package/dist/component/healthkit/transform/maps/sleepLevel.js.map +1 -0
  64. package/dist/{healthkit → component/healthkit/transform}/menstruation.d.ts +3 -17
  65. package/dist/component/healthkit/transform/menstruation.d.ts.map +1 -0
  66. package/dist/{healthkit → component/healthkit/transform}/menstruation.js +1 -1
  67. package/dist/component/healthkit/transform/menstruation.js.map +1 -0
  68. package/dist/component/healthkit/transform/nutrition.d.ts +25 -0
  69. package/dist/component/healthkit/transform/nutrition.d.ts.map +1 -0
  70. package/dist/component/healthkit/transform/nutrition.js.map +1 -0
  71. package/dist/component/healthkit/transform/sleep.d.ts +22 -0
  72. package/dist/component/healthkit/transform/sleep.d.ts.map +1 -0
  73. package/dist/{healthkit → component/healthkit/transform}/sleep.js +2 -2
  74. package/dist/component/healthkit/transform/sleep.js.map +1 -0
  75. package/dist/{healthkit → component/healthkit/transform}/utils.d.ts +1 -1
  76. package/dist/component/healthkit/transform/utils.d.ts.map +1 -0
  77. package/dist/component/healthkit/transform/utils.js.map +1 -0
  78. package/dist/component/healthkit/types.d.ts.map +1 -0
  79. package/dist/component/healthkit/types.js.map +1 -0
  80. package/dist/component/public.d.ts +3 -3
  81. package/dist/component/schema.d.ts +4 -4
  82. package/dist/component/strava/public.d.ts +4 -15
  83. package/dist/component/strava/public.d.ts.map +1 -1
  84. package/dist/component/strava/public.js +4 -3
  85. package/dist/component/strava/public.js.map +1 -1
  86. package/dist/component/strava/webhooks.d.ts +3 -10
  87. package/dist/component/strava/webhooks.d.ts.map +1 -1
  88. package/dist/component/strava/webhooks.js.map +1 -1
  89. package/dist/component/validators/daily.d.ts +2 -2
  90. package/dist/component/validators/shared.d.ts +16 -3
  91. package/dist/component/validators/shared.d.ts.map +1 -1
  92. package/dist/component/validators/shared.js +1 -1
  93. package/dist/component/validators/shared.js.map +1 -1
  94. package/dist/validators.d.ts +5 -4
  95. package/dist/validators.d.ts.map +1 -1
  96. package/dist/validators.js.map +1 -1
  97. package/package.json +3 -3
  98. package/src/client/garmin.ts +42 -0
  99. package/src/client/healthkit.ts +791 -0
  100. package/src/client/index.ts +5 -0
  101. package/src/client/types.ts +4 -2
  102. package/src/component/_generated/api.ts +26 -0
  103. package/src/component/_generated/component.ts +13 -0
  104. package/src/component/garmin/private.ts +12 -12
  105. package/src/component/garmin/public.ts +166 -11
  106. package/src/component/garmin/utils.ts +102 -0
  107. package/src/component/garmin/webhooks.ts +1 -7
  108. package/src/{healthkit → component/healthkit}/index.ts +46 -59
  109. package/src/component/healthkit/transform/activity.ts +115 -0
  110. package/src/{healthkit → component/healthkit/transform}/athlete.ts +4 -8
  111. package/src/{healthkit → component/healthkit/transform}/body.ts +3 -7
  112. package/src/{healthkit → component/healthkit/transform}/daily.ts +4 -10
  113. package/src/{healthkit/maps/menstruation-flow.ts → component/healthkit/transform/maps/menstruationFlow.ts} +1 -1
  114. package/src/{healthkit/maps/sleep-level.ts → component/healthkit/transform/maps/sleepLevel.ts} +1 -1
  115. package/src/{healthkit → component/healthkit/transform}/menstruation.ts +4 -8
  116. package/src/{healthkit → component/healthkit/transform}/nutrition.ts +3 -7
  117. package/src/{healthkit → component/healthkit/transform}/sleep.ts +5 -9
  118. package/src/{healthkit → component/healthkit/transform}/utils.ts +1 -1
  119. package/src/component/strava/public.ts +6 -5
  120. package/src/component/strava/webhooks.ts +9 -11
  121. package/src/component/validators/shared.ts +47 -4
  122. package/src/validators.ts +1 -0
  123. package/dist/healthkit/activity.d.ts +0 -75
  124. package/dist/healthkit/activity.d.ts.map +0 -1
  125. package/dist/healthkit/activity.js.map +0 -1
  126. package/dist/healthkit/athlete.d.ts.map +0 -1
  127. package/dist/healthkit/athlete.js.map +0 -1
  128. package/dist/healthkit/body.d.ts +0 -102
  129. package/dist/healthkit/body.d.ts.map +0 -1
  130. package/dist/healthkit/body.js.map +0 -1
  131. package/dist/healthkit/daily.d.ts +0 -119
  132. package/dist/healthkit/daily.d.ts.map +0 -1
  133. package/dist/healthkit/daily.js.map +0 -1
  134. package/dist/healthkit/index.d.ts +0 -21
  135. package/dist/healthkit/index.d.ts.map +0 -1
  136. package/dist/healthkit/index.js.map +0 -1
  137. package/dist/healthkit/maps/activity-type.d.ts.map +0 -1
  138. package/dist/healthkit/maps/activity-type.js.map +0 -1
  139. package/dist/healthkit/maps/menstruation-flow.d.ts.map +0 -1
  140. package/dist/healthkit/maps/menstruation-flow.js.map +0 -1
  141. package/dist/healthkit/maps/sleep-level.d.ts.map +0 -1
  142. package/dist/healthkit/maps/sleep-level.js.map +0 -1
  143. package/dist/healthkit/menstruation.d.ts.map +0 -1
  144. package/dist/healthkit/menstruation.js.map +0 -1
  145. package/dist/healthkit/nutrition.d.ts +0 -77
  146. package/dist/healthkit/nutrition.d.ts.map +0 -1
  147. package/dist/healthkit/nutrition.js.map +0 -1
  148. package/dist/healthkit/sleep.d.ts +0 -60
  149. package/dist/healthkit/sleep.d.ts.map +0 -1
  150. package/dist/healthkit/sleep.js.map +0 -1
  151. package/dist/healthkit/types.d.ts.map +0 -1
  152. package/dist/healthkit/types.js.map +0 -1
  153. package/dist/healthkit/utils.d.ts.map +0 -1
  154. package/dist/healthkit/utils.js.map +0 -1
  155. package/src/healthkit/activity.ts +0 -120
  156. /package/dist/{healthkit → component/healthkit/transform}/athlete.js +0 -0
  157. /package/dist/{healthkit → component/healthkit/transform}/body.js +0 -0
  158. /package/dist/{healthkit → component/healthkit/transform}/daily.js +0 -0
  159. /package/dist/{healthkit → component/healthkit/transform}/nutrition.js +0 -0
  160. /package/dist/{healthkit → component/healthkit/transform}/utils.js +0 -0
  161. /package/dist/{healthkit → component/healthkit}/types.d.ts +0 -0
  162. /package/dist/{healthkit → component/healthkit}/types.js +0 -0
  163. /package/src/{healthkit/maps/activity-type.ts → component/healthkit/transform/maps/activityType.ts} +0 -0
  164. /package/src/{healthkit → component/healthkit}/types.ts +0 -0
@@ -1,59 +1,46 @@
1
- // ─── @nativesquare/soma/healthkit ─────────────────────────────────────────────
2
- // Apple HealthKit → Soma schema transformers.
3
- //
4
- // Pure TypeScript functions with zero runtime dependencies.
5
- // Compatible with any HealthKit library (react-native-health, expo-health, etc.)
6
-
7
- // ── Transformers ─────────────────────────────────────────────────────────────
8
- export { transformWorkout } from "./activity.js";
9
- export type { ActivityData } from "./activity.js";
10
-
11
- export { transformSleep } from "./sleep.js";
12
- export type { SleepData } from "./sleep.js";
13
-
14
- export { transformBody } from "./body.js";
15
- export type { BodyData } from "./body.js";
16
-
17
- export { transformDaily, transformDailyFromSummary } from "./daily.js";
18
- export type { DailyData } from "./daily.js";
19
-
20
- export { transformNutrition } from "./nutrition.js";
21
- export type { NutritionData } from "./nutrition.js";
22
-
23
- export { transformMenstruation } from "./menstruation.js";
24
- export type { MenstruationData } from "./menstruation.js";
25
-
26
- export { transformAthlete } from "./athlete.js";
27
- export type { AthleteData } from "./athlete.js";
28
-
29
- // ── Enum Maps ────────────────────────────────────────────────────────────────
30
- export { mapActivityType } from "./maps/activity-type.js";
31
- export { mapSleepLevel, isAsleepCategory } from "./maps/sleep-level.js";
32
- export { mapMenstruationFlow } from "./maps/menstruation-flow.js";
33
-
34
- // ── Types ────────────────────────────────────────────────────────────────────
35
- export type {
36
- HKQuantitySample,
37
- HKCategorySample,
38
- HKWorkout,
39
- HKWorkoutRoute,
40
- HKActivitySummary,
41
- HKDevice,
42
- HKSource,
43
- HKCharacteristics,
44
- HKBiologicalSex,
45
- HKQuantityTypeIdentifier,
46
- HKCategoryTypeIdentifier,
47
- HKSleepCategoryValue,
48
- HKMenstrualFlowCategoryValue,
49
- } from "./types.js";
50
-
51
- export { HKSleepCategory, HKMenstrualFlowCategory } from "./types.js";
52
-
53
- // ── Utilities ────────────────────────────────────────────────────────────────
54
- export {
55
- diffSeconds,
56
- dayRange,
57
- sampleTimeRange,
58
- buildDeviceData,
59
- } from "./utils.js";
1
+ // ─── @nativesquare/soma/healthkit ─────────────────────────────────────────────
2
+ // Apple HealthKit → Soma schema transformers.
3
+ //
4
+ // Pure TypeScript functions with zero runtime dependencies.
5
+ // Compatible with any HealthKit library (react-native-health, expo-health, etc.)
6
+
7
+ // ── Transformers ─────────────────────────────────────────────────────────────
8
+ export { transformWorkout } from "./transform/activity.js";
9
+ export { transformSleep } from "./transform/sleep.js";
10
+ export { transformBody } from "./transform/body.js";
11
+ export { transformDaily, transformDailyFromSummary } from "./transform/daily.js";
12
+ export { transformNutrition } from "./transform/nutrition.js";
13
+ export { transformMenstruation } from "./transform/menstruation.js";
14
+ export { transformAthlete } from "./transform/athlete.js";
15
+
16
+ // ── Enum Maps ────────────────────────────────────────────────────────────────
17
+ export { mapActivityType } from "./transform/maps/activityType.js";
18
+ export { mapSleepLevel, isAsleepCategory } from "./transform/maps/sleepLevel.js";
19
+ export { mapMenstruationFlow } from "./transform/maps/menstruationFlow.js";
20
+
21
+ // ── Types ────────────────────────────────────────────────────────────────────
22
+ export type {
23
+ HKQuantitySample,
24
+ HKCategorySample,
25
+ HKWorkout,
26
+ HKWorkoutRoute,
27
+ HKActivitySummary,
28
+ HKDevice,
29
+ HKSource,
30
+ HKCharacteristics,
31
+ HKBiologicalSex,
32
+ HKQuantityTypeIdentifier,
33
+ HKCategoryTypeIdentifier,
34
+ HKSleepCategoryValue,
35
+ HKMenstrualFlowCategoryValue,
36
+ } from "./types.js";
37
+
38
+ export { HKSleepCategory, HKMenstrualFlowCategory } from "./types.js";
39
+
40
+ // ── Utilities ────────────────────────────────────────────────────────────────
41
+ export {
42
+ diffSeconds,
43
+ dayRange,
44
+ sampleTimeRange,
45
+ buildDeviceData,
46
+ } from "./transform/utils.js";
@@ -0,0 +1,115 @@
1
+ // ─── Activity Transformer ────────────────────────────────────────────────────
2
+ // Transforms an Apple HealthKit HKWorkout into the Soma Activity schema shape.
3
+
4
+ import type { HKWorkout } from "../types.js";
5
+ import type { SomaActivity } from "../../validators/activity.js";
6
+ import { mapActivityType } from "./maps/activityType.js";
7
+ import { buildDeviceData } from "./utils.js";
8
+
9
+ /**
10
+ * Transform an HKWorkout 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
+ * @param workout - The HKWorkout from HealthKit
16
+ * @returns Soma Activity fields (without connectionId/userId)
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const data = transformWorkout(hkWorkout);
21
+ * await soma.ingestActivity(ctx, { connectionId, userId, ...data });
22
+ * ```
23
+ */
24
+ export function transformWorkout(workout: HKWorkout): SomaActivity {
25
+ const heartRateSamples = workout.heartRateSamples;
26
+ const hrValues = heartRateSamples?.map((s) => s.value) ?? [];
27
+
28
+ return {
29
+ metadata: {
30
+ summary_id: workout.uuid,
31
+ start_time: workout.startDate,
32
+ end_time: workout.endDate,
33
+ type: mapActivityType(workout.workoutActivityType),
34
+ upload_type: 1 as const, // Automatic
35
+ name: undefined as string | undefined,
36
+ },
37
+
38
+ active_durations_data: {
39
+ activity_seconds: workout.duration,
40
+ },
41
+
42
+ calories_data:
43
+ workout.totalEnergyBurned != null
44
+ ? {
45
+ total_burned_calories: workout.totalEnergyBurned,
46
+ }
47
+ : undefined,
48
+
49
+ device_data: buildDeviceData(workout.source, workout.device),
50
+
51
+ distance_data:
52
+ workout.totalDistance != null ||
53
+ workout.totalSwimmingStrokeCount != null ||
54
+ workout.totalFlightsClimbed != null
55
+ ? {
56
+ summary: {
57
+ distance_meters: workout.totalDistance,
58
+ steps: undefined as number | undefined,
59
+ floors_climbed: workout.totalFlightsClimbed,
60
+ swimming:
61
+ workout.totalSwimmingStrokeCount != null
62
+ ? { num_strokes: workout.totalSwimmingStrokeCount }
63
+ : undefined,
64
+ },
65
+ }
66
+ : undefined,
67
+
68
+ heart_rate_data:
69
+ heartRateSamples && heartRateSamples.length > 0
70
+ ? {
71
+ detailed: {
72
+ hr_samples: heartRateSamples.map((s) => ({
73
+ timestamp: s.startDate,
74
+ bpm: s.value,
75
+ })),
76
+ },
77
+ summary: {
78
+ avg_hr_bpm:
79
+ hrValues.length > 0
80
+ ? hrValues.reduce((a, b) => a + b, 0) / hrValues.length
81
+ : undefined,
82
+ max_hr_bpm:
83
+ hrValues.length > 0 ? Math.max(...hrValues) : undefined,
84
+ min_hr_bpm:
85
+ hrValues.length > 0 ? Math.min(...hrValues) : undefined,
86
+ },
87
+ }
88
+ : undefined,
89
+
90
+ position_data:
91
+ workout.routeData && workout.routeData.length > 0
92
+ ? {
93
+ position_samples: workout.routeData.flatMap((route) =>
94
+ route.locations.map((loc) => ({
95
+ timestamp: loc.timestamp,
96
+ coords_lat_lng_deg: [loc.latitude, loc.longitude],
97
+ })),
98
+ ),
99
+ start_pos_lat_lng_deg: (() => {
100
+ const first = workout.routeData[0]?.locations[0];
101
+ return first
102
+ ? [first.latitude, first.longitude]
103
+ : undefined;
104
+ })(),
105
+ end_pos_lat_lng_deg: (() => {
106
+ const lastRoute =
107
+ workout.routeData[workout.routeData.length - 1];
108
+ const last =
109
+ lastRoute?.locations[lastRoute.locations.length - 1];
110
+ return last ? [last.latitude, last.longitude] : undefined;
111
+ })(),
112
+ }
113
+ : undefined,
114
+ };
115
+ }
@@ -2,12 +2,8 @@
2
2
  // Transforms Apple HealthKit user characteristics into the Soma Athlete schema.
3
3
  // NOTE: HealthKit exposes very limited profile data compared to other providers.
4
4
 
5
- import type { HKCharacteristics } from "./types.js";
6
-
7
- /**
8
- * The output shape of {@link transformAthlete}.
9
- */
10
- export type AthleteData = ReturnType<typeof transformAthlete>;
5
+ import type { SomaAthlete } from "../../validators/athlete.js";
6
+ import type { HKCharacteristics } from "../types.js";
11
7
 
12
8
  /**
13
9
  * Transform HealthKit user characteristics into a Soma Athlete document shape.
@@ -25,7 +21,7 @@ export type AthleteData = ReturnType<typeof transformAthlete>;
25
21
  * await soma.ingestAthlete(ctx, { connectionId, userId, ...data });
26
22
  * ```
27
23
  */
28
- export function transformAthlete(characteristics: HKCharacteristics) {
24
+ export function transformAthlete(characteristics: HKCharacteristics): SomaAthlete {
29
25
  const sexMap: Record<string, string> = {
30
26
  female: "female",
31
27
  male: "male",
@@ -35,7 +31,7 @@ export function transformAthlete(characteristics: HKCharacteristics) {
35
31
  return {
36
32
  sex:
37
33
  characteristics.biologicalSex &&
38
- characteristics.biologicalSex !== "notSet"
34
+ characteristics.biologicalSex !== "notSet"
39
35
  ? sexMap[characteristics.biologicalSex]
40
36
  : undefined,
41
37
  date_of_birth: characteristics.dateOfBirth,
@@ -1,7 +1,8 @@
1
1
  // ─── Body Transformer ────────────────────────────────────────────────────────
2
2
  // Transforms Apple HealthKit body-related quantity samples into the Soma Body schema.
3
3
 
4
- import type { HKQuantitySample } from "./types.js";
4
+ import type { HKQuantitySample } from "../types.js";
5
+ import type { SomaBody } from "../../validators/body.js";
5
6
  import {
6
7
  filterByType,
7
8
  avgValue,
@@ -11,11 +12,6 @@ import {
11
12
  buildDeviceData,
12
13
  } from "./utils.js";
13
14
 
14
- /**
15
- * The output shape of {@link transformBody}.
16
- */
17
- export type BodyData = ReturnType<typeof transformBody>;
18
-
19
15
  /**
20
16
  * Transform a mixed array of HealthKit body-related quantity samples into a
21
17
  * Soma Body document shape.
@@ -37,7 +33,7 @@ export type BodyData = ReturnType<typeof transformBody>;
37
33
  export function transformBody(
38
34
  samples: HKQuantitySample[],
39
35
  timeRange?: { start_time: string; end_time: string },
40
- ) {
36
+ ): SomaBody {
41
37
  const range = timeRange ?? sampleTimeRange(samples);
42
38
 
43
39
  // ── Heart rate ─────────────────────────────────────────────────────────
@@ -1,7 +1,8 @@
1
1
  // ─── Daily Transformer ───────────────────────────────────────────────────────
2
2
  // Transforms Apple HealthKit daily activity data into the Soma Daily schema.
3
3
 
4
- import type { HKQuantitySample, HKActivitySummary } from "./types.js";
4
+ import type { HKQuantitySample, HKActivitySummary } from "../types.js";
5
+ import type { SomaDaily } from "../../validators/daily.js";
5
6
  import {
6
7
  filterByType,
7
8
  sumValues,
@@ -11,13 +12,6 @@ import {
11
12
  buildDeviceData,
12
13
  } from "./utils.js";
13
14
 
14
- /**
15
- * The output shape of {@link transformDaily} and {@link transformDailyFromSummary}.
16
- */
17
- export type DailyData =
18
- | ReturnType<typeof transformDaily>
19
- | ReturnType<typeof transformDailyFromSummary>;
20
-
21
15
  /**
22
16
  * Transform an array of HealthKit quantity samples for a single day into a
23
17
  * Soma Daily document shape.
@@ -39,7 +33,7 @@ export type DailyData =
39
33
  export function transformDaily(
40
34
  samples: HKQuantitySample[],
41
35
  timeRange?: { start_time: string; end_time: string },
42
- ) {
36
+ ): SomaDaily {
43
37
  const range = timeRange ?? sampleTimeRange(samples);
44
38
 
45
39
  // ── Activity samples ───────────────────────────────────────────────────
@@ -222,7 +216,7 @@ export function transformDaily(
222
216
  * @param summary - The HKActivitySummary from HealthKit
223
217
  * @returns Soma Daily fields (without connectionId/userId)
224
218
  */
225
- export function transformDailyFromSummary(summary: HKActivitySummary) {
219
+ export function transformDailyFromSummary(summary: HKActivitySummary): SomaDaily {
226
220
  const range = dayRange(summary.dateComponents);
227
221
 
228
222
  return {
@@ -4,7 +4,7 @@
4
4
  // HK values: https://developer.apple.com/documentation/healthkit/hkcategoryvaluemenstrualflow
5
5
  // Terra MenstruationFlow: 0=UNKNOWN, 1=NONE, 2=LIGHT, 3=MEDIUM, 4=HEAVY, 5=HAD
6
6
 
7
- import { HKMenstrualFlowCategory } from "../types.js";
7
+ import { HKMenstrualFlowCategory } from "../../types.js";
8
8
 
9
9
  const menstruationFlowMap: Record<number, number> = {
10
10
  [HKMenstrualFlowCategory.Unspecified]: 5, // Unspecified → HAD (flow occurred, amount unknown)
@@ -4,7 +4,7 @@
4
4
  // HK values: https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis
5
5
  // Terra SleepLevel: 0=Unknown, 1=Awake, 2=Sleeping, 3=OutOfBed, 4=Light, 5=Deep, 6=REM
6
6
 
7
- import { HKSleepCategory } from "../types.js";
7
+ import { HKSleepCategory } from "../../types.js";
8
8
 
9
9
  const sleepLevelMap: Record<number, number> = {
10
10
  [HKSleepCategory.InBed]: 2, // InBed → Sleeping (generic)
@@ -1,15 +1,11 @@
1
1
  // ─── Menstruation Transformer ─────────────────────────────────────────────────
2
2
  // Transforms Apple HealthKit menstrual flow samples into the Soma Menstruation schema.
3
3
 
4
- import type { HKCategorySample } from "./types.js";
5
- import { mapMenstruationFlow } from "./maps/menstruation-flow.js";
4
+ import type { HKCategorySample } from "../types.js";
5
+ import type { SomaMenstruation } from "../../validators/menstruation.js";
6
+ import { mapMenstruationFlow } from "./maps/menstruationFlow.js";
6
7
  import { sampleTimeRange } from "./utils.js";
7
8
 
8
- /**
9
- * The output shape of {@link transformMenstruation}.
10
- */
11
- export type MenstruationData = ReturnType<typeof transformMenstruation>;
12
-
13
9
  /**
14
10
  * Transform an array of HealthKit menstrual flow category samples into a
15
11
  * Soma Menstruation document shape.
@@ -27,7 +23,7 @@ export type MenstruationData = ReturnType<typeof transformMenstruation>;
27
23
  export function transformMenstruation(
28
24
  samples: HKCategorySample[],
29
25
  timeRange?: { start_time: string; end_time: string },
30
- ) {
26
+ ): SomaMenstruation {
31
27
  if (samples.length === 0) {
32
28
  throw new Error(
33
29
  "transformMenstruation requires at least one menstrual flow sample",
@@ -1,14 +1,10 @@
1
1
  // ─── Nutrition Transformer ────────────────────────────────────────────────────
2
2
  // Transforms Apple HealthKit dietary quantity samples into the Soma Nutrition schema.
3
3
 
4
- import type { HKQuantitySample } from "./types.js";
4
+ import type { HKQuantitySample } from "../types.js";
5
+ import type { SomaNutrition } from "../../validators/nutrition.js";
5
6
  import { filterByType, sumValues, sampleTimeRange } from "./utils.js";
6
7
 
7
- /**
8
- * The output shape of {@link transformNutrition}.
9
- */
10
- export type NutritionData = ReturnType<typeof transformNutrition>;
11
-
12
8
  /**
13
9
  * Transform an array of HealthKit dietary quantity samples into a
14
10
  * Soma Nutrition document shape.
@@ -30,7 +26,7 @@ export type NutritionData = ReturnType<typeof transformNutrition>;
30
26
  export function transformNutrition(
31
27
  samples: HKQuantitySample[],
32
28
  timeRange?: { start_time: string; end_time: string },
33
- ) {
29
+ ): SomaNutrition {
34
30
  const range = timeRange ?? sampleTimeRange(samples);
35
31
 
36
32
  const sum = (type: string) => {
@@ -1,16 +1,12 @@
1
1
  // ─── Sleep Transformer ───────────────────────────────────────────────────────
2
2
  // Transforms Apple HealthKit sleep analysis samples into the Soma Sleep schema.
3
3
 
4
- import type { HKCategorySample } from "./types.js";
5
- import { HKSleepCategory } from "./types.js";
6
- import { mapSleepLevel, isAsleepCategory } from "./maps/sleep-level.js";
4
+ import type { HKCategorySample } from "../types.js";
5
+ import type { SomaSleep } from "../../validators/sleep.js";
6
+ import { HKSleepCategory } from "../types.js";
7
+ import { mapSleepLevel, isAsleepCategory } from "./maps/sleepLevel.js";
7
8
  import { diffSeconds, buildDeviceData } from "./utils.js";
8
9
 
9
- /**
10
- * The output shape of {@link transformSleep}.
11
- */
12
- export type SleepData = ReturnType<typeof transformSleep>;
13
-
14
10
  /**
15
11
  * Transform an array of HealthKit sleep analysis category samples into a
16
12
  * Soma Sleep document shape.
@@ -29,7 +25,7 @@ export type SleepData = ReturnType<typeof transformSleep>;
29
25
  * await soma.ingestSleep(ctx, { connectionId, userId, ...data });
30
26
  * ```
31
27
  */
32
- export function transformSleep(samples: HKCategorySample[]) {
28
+ export function transformSleep(samples: HKCategorySample[]): SomaSleep {
33
29
  if (samples.length === 0) {
34
30
  throw new Error("transformSleep requires at least one sleep sample");
35
31
  }
@@ -1,7 +1,7 @@
1
1
  // ─── Shared Utilities ────────────────────────────────────────────────────────
2
2
  // Pure helper functions used across HealthKit transformer modules.
3
3
 
4
- import type { HKDevice, HKQuantitySample, HKSource } from "./types.js";
4
+ import type { HKDevice, HKQuantitySample, HKSource } from "../types.js";
5
5
 
6
6
  /**
7
7
  * Compute the difference in seconds between two ISO-8601 timestamps.
@@ -21,7 +21,7 @@ import {
21
21
  } from "./auth.js";
22
22
  import { transformActivity } from "./transform/activity.js";
23
23
  import { transformAthlete } from "./transform/athlete.js";
24
- import type { SomaError } from "../validators/shared.js";
24
+ import type { SomaError, SomaErrorType } from "../validators/shared.js";
25
25
 
26
26
  // ─── OAuth ──────────────────────────────────────────────────────────────────
27
27
 
@@ -153,8 +153,8 @@ export const disconnectStrava = action({
153
153
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
154
154
  body: `access_token=${tokenDoc.accessToken}`,
155
155
  });
156
- } catch {
157
- // Deauthorization is best-effort clean up locally regardless
156
+ } catch (err) {
157
+ console.warn("[strava:disconnect] Best-effort deauthorization failed:", err instanceof Error ? err.message : err);
158
158
  }
159
159
 
160
160
  // 3. Delete stored tokens
@@ -303,9 +303,10 @@ export const pullAll = action({
303
303
  clientId: args.clientId,
304
304
  clientSecret: args.clientSecret,
305
305
  };
306
- const pullFns = [
306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
+ const pullFns: Array<{ ref: any; name: SomaErrorType; args: Record<string, unknown> }> = [
307
308
  { ref: api.strava.public.pullAthlete, name: "athlete", args: sharedArgs },
308
- { ref: api.strava.public.pullActivities, name: "activities", args: { ...sharedArgs, after: args.after, before: args.before } },
309
+ { ref: api.strava.public.pullActivities, name: "activity", args: { ...sharedArgs, after: args.after, before: args.before } },
309
310
  ];
310
311
  const synced: Record<string, number> = {};
311
312
  const errors: SomaError[] = [];
@@ -17,7 +17,7 @@ import {
17
17
  } from "./types/stravaApi/sdk.gen.js";
18
18
  import { transformActivity } from "./transform/activity.js";
19
19
  import { transformAthlete } from "./transform/athlete.js";
20
- import type { SomaError } from "../validators/shared.js";
20
+ import type { SomaError, SomaErrorType, WebhookResult } from "../validators/shared.js";
21
21
 
22
22
  // ─── Schema ─────────────────────────────────────────────────────────────────
23
23
 
@@ -41,10 +41,8 @@ const VALID_EVENT_NAMES = new Set([
41
41
  "athlete-deauthorize",
42
42
  ]);
43
43
 
44
- type WebhookResult = {
45
- errors: SomaError[];
46
- items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> | null }>;
47
- };
44
+ // Strava webhooks use WebhookResult with nullable data (for delete/deauthorize events).
45
+ type StravaWebhookResult = WebhookResult<Record<string, unknown> | null>;
48
46
 
49
47
  // ─── Handler ────────────────────────────────────────────────────────────────
50
48
 
@@ -55,7 +53,7 @@ export const handleStravaWebhook = action({
55
53
  clientSecret: v.string(),
56
54
  autoIngest: v.optional(v.boolean()),
57
55
  },
58
- handler: async (ctx, args): Promise<WebhookResult> => {
56
+ handler: async (ctx, args): Promise<StravaWebhookResult> => {
59
57
  const payload = stravaWebhookPayloadSchema.parse(args.payload);
60
58
  const { clientId, clientSecret } = args;
61
59
  const shouldIngest = args.autoIngest !== false;
@@ -74,7 +72,7 @@ export const handleStravaWebhook = action({
74
72
  if (!connection) {
75
73
  return {
76
74
  errors: [{
77
- type: payload.object_type,
75
+ type: payload.object_type as SomaErrorType,
78
76
  id: String(payload.object_id),
79
77
  message: `No Soma connection found for Strava owner_id "${payload.owner_id}". ` +
80
78
  "The user may need to reconnect to populate the provider user ID.",
@@ -87,7 +85,7 @@ export const handleStravaWebhook = action({
87
85
  if (!connection.active && eventName !== "athlete-deauthorize") {
88
86
  return {
89
87
  errors: [{
90
- type: payload.object_type,
88
+ type: payload.object_type as SomaErrorType,
91
89
  id: String(payload.object_id),
92
90
  message: `Strava connection for owner_id "${payload.owner_id}" is inactive`,
93
91
  }],
@@ -141,7 +139,7 @@ async function handleActivityCreateOrUpdate(
141
139
  clientSecret: string;
142
140
  shouldIngest: boolean;
143
141
  },
144
- ): Promise<WebhookResult> {
142
+ ): Promise<StravaWebhookResult> {
145
143
  const errors: SomaError[] = [];
146
144
 
147
145
  let accessToken: string;
@@ -219,7 +217,7 @@ async function handleAthleteUpdate(
219
217
  clientSecret: string;
220
218
  shouldIngest: boolean;
221
219
  },
222
- ): Promise<WebhookResult> {
220
+ ): Promise<StravaWebhookResult> {
223
221
  let accessToken: string;
224
222
  try {
225
223
  const resolved = await ctx.runAction(
@@ -283,7 +281,7 @@ async function handleAthleteDeauthorize(
283
281
  userId: string;
284
282
  shouldIngest: boolean;
285
283
  },
286
- ): Promise<WebhookResult> {
284
+ ): Promise<StravaWebhookResult> {
287
285
  if (args.shouldIngest) {
288
286
  try {
289
287
  await ctx.runMutation(internal.private.deleteTokens, {
@@ -1,15 +1,58 @@
1
- import { type Infer, v } from "convex/values";
1
+ import { v } from "convex/values";
2
+
3
+ // ─── SomaErrorType ──────────────────────────────────────────────────────────
4
+ // Canonical error‐type identifiers shared across Garmin & Strava modules.
5
+
6
+ export type SomaErrorType =
7
+ // Data domain types
8
+ | "activity"
9
+ | "activityDetails"
10
+ | "athlete"
11
+ | "bloodPressure"
12
+ | "body"
13
+ | "daily"
14
+ | "epochs"
15
+ | "healthSnapshot"
16
+ | "hrv"
17
+ | "manuallyUpdatedActivities"
18
+ | "menstruation"
19
+ | "moveIQ"
20
+ | "pulseOx"
21
+ | "respiration"
22
+ | "skinTemperature"
23
+ | "sleep"
24
+ | "stressDetails"
25
+ | "userMetrics"
26
+ // Operation types
27
+ | "deleteSchedule"
28
+ | "deleteWorkout"
29
+ | "ingest"
30
+ | "pushSchedule"
31
+ | "pushWorkout";
2
32
 
3
33
  // ─── SomaError ──────────────────────────────────────────────────────────────
4
- // Convex validator matching the SomaError TypeScript interface.
34
+
35
+ /** Convex validator for SomaError (uses v.string() for runtime flexibility). */
5
36
  export const somaErrorValidator = v.object({
6
37
  type: v.string(),
7
38
  id: v.string(),
8
39
  message: v.string(),
9
40
  });
10
41
 
11
- /** Structured error from a Soma operation, derived from the Convex validator. */
12
- export type SomaError = Infer<typeof somaErrorValidator>;
42
+ /** Structured error from a Soma operation. */
43
+ export interface SomaError {
44
+ type: SomaErrorType;
45
+ id: string;
46
+ message: string;
47
+ }
48
+
49
+ // ─── WebhookResult ──────────────────────────────────────────────────────────
50
+ // Shared return shape for all webhook handler actions.
51
+
52
+ export type WebhookResult<TData = Record<string, unknown>> = {
53
+ errors: SomaError[];
54
+ items: Array<{ connectionId: string; userId: string; data: TData }>;
55
+ };
13
56
 
14
57
  import {
15
58
  heartRateDataSample,
package/src/validators.ts CHANGED
@@ -40,6 +40,7 @@
40
40
 
41
41
  import { v } from "convex/values";
42
42
  export { somaErrorValidator } from "./component/validators/shared.js";
43
+ export type { SomaErrorType } from "./component/validators/shared.js";
43
44
  import { connectionValidator as _connectionValidator } from "./component/validators/connection.js";
44
45
  import {
45
46
  activityValidator as _activityValidator,