@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.
- package/dist/client/garmin.d.ts +31 -0
- package/dist/client/garmin.d.ts.map +1 -1
- package/dist/client/garmin.js +34 -0
- package/dist/client/garmin.js.map +1 -1
- package/dist/client/healthkit.d.ts +267 -0
- package/dist/client/healthkit.d.ts.map +1 -0
- package/dist/client/healthkit.js +600 -0
- package/dist/client/healthkit.js.map +1 -0
- package/dist/client/index.d.ts +4 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +3 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +26 -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 +7 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/private.d.ts +18 -85
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +12 -12
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +38 -65
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +132 -10
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/utils.d.ts +48 -0
- package/dist/component/garmin/utils.d.ts.map +1 -1
- package/dist/component/garmin/utils.js +65 -0
- package/dist/component/garmin/utils.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +1 -11
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/healthkit/index.d.ts +14 -0
- package/dist/component/healthkit/index.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit}/index.js +11 -11
- package/dist/component/healthkit/index.js.map +1 -0
- package/dist/component/healthkit/transform/activity.d.ts +19 -0
- package/dist/component/healthkit/transform/activity.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/activity.js +1 -1
- package/dist/component/healthkit/transform/activity.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/athlete.d.ts +3 -9
- package/dist/component/healthkit/transform/athlete.d.ts.map +1 -0
- package/dist/component/healthkit/transform/athlete.js.map +1 -0
- package/dist/component/healthkit/transform/body.d.ts +25 -0
- package/dist/component/healthkit/transform/body.d.ts.map +1 -0
- package/dist/component/healthkit/transform/body.js.map +1 -0
- package/dist/component/healthkit/transform/daily.d.ts +36 -0
- package/dist/component/healthkit/transform/daily.d.ts.map +1 -0
- package/dist/component/healthkit/transform/daily.js.map +1 -0
- package/dist/{healthkit/maps/activity-type.d.ts → component/healthkit/transform/maps/activityType.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/activityType.d.ts.map +1 -0
- package/dist/{healthkit/maps/activity-type.js → component/healthkit/transform/maps/activityType.js} +1 -1
- package/dist/component/healthkit/transform/maps/activityType.js.map +1 -0
- package/dist/{healthkit/maps/menstruation-flow.d.ts → component/healthkit/transform/maps/menstruationFlow.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/menstruationFlow.d.ts.map +1 -0
- package/dist/{healthkit/maps/menstruation-flow.js → component/healthkit/transform/maps/menstruationFlow.js} +2 -2
- package/dist/component/healthkit/transform/maps/menstruationFlow.js.map +1 -0
- package/dist/{healthkit/maps/sleep-level.d.ts → component/healthkit/transform/maps/sleepLevel.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/sleepLevel.d.ts.map +1 -0
- package/dist/{healthkit/maps/sleep-level.js → component/healthkit/transform/maps/sleepLevel.js} +2 -2
- package/dist/component/healthkit/transform/maps/sleepLevel.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/menstruation.d.ts +3 -17
- package/dist/component/healthkit/transform/menstruation.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/menstruation.js +1 -1
- package/dist/component/healthkit/transform/menstruation.js.map +1 -0
- package/dist/component/healthkit/transform/nutrition.d.ts +25 -0
- package/dist/component/healthkit/transform/nutrition.d.ts.map +1 -0
- package/dist/component/healthkit/transform/nutrition.js.map +1 -0
- package/dist/component/healthkit/transform/sleep.d.ts +22 -0
- package/dist/component/healthkit/transform/sleep.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/sleep.js +2 -2
- package/dist/component/healthkit/transform/sleep.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/utils.d.ts +1 -1
- package/dist/component/healthkit/transform/utils.d.ts.map +1 -0
- package/dist/component/healthkit/transform/utils.js.map +1 -0
- package/dist/component/healthkit/types.d.ts.map +1 -0
- package/dist/component/healthkit/types.js.map +1 -0
- package/dist/component/public.d.ts +3 -3
- package/dist/component/schema.d.ts +4 -4
- package/dist/component/strava/public.d.ts +4 -15
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +4 -3
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/webhooks.d.ts +3 -10
- package/dist/component/strava/webhooks.d.ts.map +1 -1
- package/dist/component/strava/webhooks.js.map +1 -1
- package/dist/component/validators/daily.d.ts +2 -2
- package/dist/component/validators/shared.d.ts +16 -3
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +1 -1
- package/dist/component/validators/shared.js.map +1 -1
- package/dist/validators.d.ts +5 -4
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js.map +1 -1
- package/package.json +3 -3
- package/src/client/garmin.ts +42 -0
- package/src/client/healthkit.ts +791 -0
- package/src/client/index.ts +5 -0
- package/src/client/types.ts +4 -2
- package/src/component/_generated/api.ts +26 -0
- package/src/component/_generated/component.ts +13 -0
- package/src/component/garmin/private.ts +12 -12
- package/src/component/garmin/public.ts +166 -11
- package/src/component/garmin/utils.ts +102 -0
- package/src/component/garmin/webhooks.ts +1 -7
- package/src/{healthkit → component/healthkit}/index.ts +46 -59
- package/src/component/healthkit/transform/activity.ts +115 -0
- package/src/{healthkit → component/healthkit/transform}/athlete.ts +4 -8
- package/src/{healthkit → component/healthkit/transform}/body.ts +3 -7
- package/src/{healthkit → component/healthkit/transform}/daily.ts +4 -10
- package/src/{healthkit/maps/menstruation-flow.ts → component/healthkit/transform/maps/menstruationFlow.ts} +1 -1
- package/src/{healthkit/maps/sleep-level.ts → component/healthkit/transform/maps/sleepLevel.ts} +1 -1
- package/src/{healthkit → component/healthkit/transform}/menstruation.ts +4 -8
- package/src/{healthkit → component/healthkit/transform}/nutrition.ts +3 -7
- package/src/{healthkit → component/healthkit/transform}/sleep.ts +5 -9
- package/src/{healthkit → component/healthkit/transform}/utils.ts +1 -1
- package/src/component/strava/public.ts +6 -5
- package/src/component/strava/webhooks.ts +9 -11
- package/src/component/validators/shared.ts +47 -4
- package/src/validators.ts +1 -0
- package/dist/healthkit/activity.d.ts +0 -75
- package/dist/healthkit/activity.d.ts.map +0 -1
- package/dist/healthkit/activity.js.map +0 -1
- package/dist/healthkit/athlete.d.ts.map +0 -1
- package/dist/healthkit/athlete.js.map +0 -1
- package/dist/healthkit/body.d.ts +0 -102
- package/dist/healthkit/body.d.ts.map +0 -1
- package/dist/healthkit/body.js.map +0 -1
- package/dist/healthkit/daily.d.ts +0 -119
- package/dist/healthkit/daily.d.ts.map +0 -1
- package/dist/healthkit/daily.js.map +0 -1
- package/dist/healthkit/index.d.ts +0 -21
- package/dist/healthkit/index.d.ts.map +0 -1
- package/dist/healthkit/index.js.map +0 -1
- package/dist/healthkit/maps/activity-type.d.ts.map +0 -1
- package/dist/healthkit/maps/activity-type.js.map +0 -1
- package/dist/healthkit/maps/menstruation-flow.d.ts.map +0 -1
- package/dist/healthkit/maps/menstruation-flow.js.map +0 -1
- package/dist/healthkit/maps/sleep-level.d.ts.map +0 -1
- package/dist/healthkit/maps/sleep-level.js.map +0 -1
- package/dist/healthkit/menstruation.d.ts.map +0 -1
- package/dist/healthkit/menstruation.js.map +0 -1
- package/dist/healthkit/nutrition.d.ts +0 -77
- package/dist/healthkit/nutrition.d.ts.map +0 -1
- package/dist/healthkit/nutrition.js.map +0 -1
- package/dist/healthkit/sleep.d.ts +0 -60
- package/dist/healthkit/sleep.d.ts.map +0 -1
- package/dist/healthkit/sleep.js.map +0 -1
- package/dist/healthkit/types.d.ts.map +0 -1
- package/dist/healthkit/types.js.map +0 -1
- package/dist/healthkit/utils.d.ts.map +0 -1
- package/dist/healthkit/utils.js.map +0 -1
- package/src/healthkit/activity.ts +0 -120
- /package/dist/{healthkit → component/healthkit/transform}/athlete.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/body.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/daily.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/nutrition.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/utils.js +0 -0
- /package/dist/{healthkit → component/healthkit}/types.d.ts +0 -0
- /package/dist/{healthkit → component/healthkit}/types.js +0 -0
- /package/src/{healthkit/maps/activity-type.ts → component/healthkit/transform/maps/activityType.ts} +0 -0
- /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
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 {
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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)
|
package/src/{healthkit/maps/sleep-level.ts → component/healthkit/transform/maps/sleepLevel.ts}
RENAMED
|
@@ -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 "
|
|
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 "
|
|
5
|
-
import {
|
|
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 "
|
|
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 "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
45
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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
|
|
12
|
-
export
|
|
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,
|