@nativesquare/soma 0.7.3 → 0.9.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/index.d.ts +17 -55
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +63 -13
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +80 -4
- 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 +153 -120
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/private.d.ts +475 -0
- package/dist/component/garmin/private.d.ts.map +1 -0
- package/dist/component/garmin/private.js +1614 -0
- package/dist/component/garmin/private.js.map +1 -0
- package/dist/component/{garmin.d.ts → garmin/public.d.ts} +25 -52
- package/dist/component/garmin/public.d.ts.map +1 -0
- package/dist/component/{garmin.js → garmin/public.js} +301 -215
- package/dist/component/garmin/public.js.map +1 -0
- package/dist/component/garmin/schemas/activity.d.ts +94 -0
- package/dist/component/garmin/schemas/activity.d.ts.map +1 -0
- package/dist/component/garmin/schemas/activity.js +27 -0
- package/dist/component/garmin/schemas/activity.js.map +1 -0
- package/dist/component/garmin/schemas/activityDetails.d.ts +146 -0
- package/dist/component/garmin/schemas/activityDetails.d.ts.map +1 -0
- package/dist/component/garmin/schemas/activityDetails.js +27 -0
- package/dist/component/garmin/schemas/activityDetails.js.map +1 -0
- package/dist/component/garmin/schemas/bloodPressure.d.ts +38 -0
- package/dist/component/garmin/schemas/bloodPressure.d.ts.map +1 -0
- package/dist/component/garmin/schemas/bloodPressure.js +27 -0
- package/dist/component/garmin/schemas/bloodPressure.js.map +1 -0
- package/dist/component/garmin/schemas/bodyCompositions.d.ts +42 -0
- package/dist/component/garmin/schemas/bodyCompositions.d.ts.map +1 -0
- package/dist/component/garmin/schemas/bodyCompositions.js +27 -0
- package/dist/component/garmin/schemas/bodyCompositions.js.map +1 -0
- package/dist/component/garmin/schemas/dailies.d.ts +98 -0
- package/dist/component/garmin/schemas/dailies.d.ts.map +1 -0
- package/dist/component/garmin/schemas/dailies.js +27 -0
- package/dist/component/garmin/schemas/dailies.js.map +1 -0
- package/dist/component/garmin/schemas/epochs.d.ts +54 -0
- package/dist/component/garmin/schemas/epochs.d.ts.map +1 -0
- package/dist/component/garmin/schemas/epochs.js +27 -0
- package/dist/component/garmin/schemas/epochs.js.map +1 -0
- package/dist/component/garmin/schemas/healthSnapshot.d.ts +48 -0
- package/dist/component/garmin/schemas/healthSnapshot.d.ts.map +1 -0
- package/dist/component/garmin/schemas/healthSnapshot.js +27 -0
- package/dist/component/garmin/schemas/healthSnapshot.js.map +1 -0
- package/dist/component/garmin/schemas/hrvSummary.d.ts +40 -0
- package/dist/component/garmin/schemas/hrvSummary.d.ts.map +1 -0
- package/dist/component/garmin/schemas/hrvSummary.js +27 -0
- package/dist/component/garmin/schemas/hrvSummary.js.map +1 -0
- package/dist/component/garmin/schemas/manuallyUpdatedActivities.d.ts +94 -0
- package/dist/component/garmin/schemas/manuallyUpdatedActivities.d.ts.map +1 -0
- package/dist/component/garmin/schemas/manuallyUpdatedActivities.js +27 -0
- package/dist/component/garmin/schemas/manuallyUpdatedActivities.js.map +1 -0
- package/dist/component/garmin/schemas/menstrualCycleTracking.d.ts +100 -0
- package/dist/component/garmin/schemas/menstrualCycleTracking.d.ts.map +1 -0
- package/dist/component/garmin/schemas/menstrualCycleTracking.js +28 -0
- package/dist/component/garmin/schemas/menstrualCycleTracking.js.map +1 -0
- package/dist/component/garmin/schemas/moveIQ.d.ts +40 -0
- package/dist/component/garmin/schemas/moveIQ.d.ts.map +1 -0
- package/dist/component/garmin/schemas/moveIQ.js +27 -0
- package/dist/component/garmin/schemas/moveIQ.js.map +1 -0
- package/dist/component/garmin/schemas/pulseOx.d.ts +38 -0
- package/dist/component/garmin/schemas/pulseOx.d.ts.map +1 -0
- package/dist/component/garmin/schemas/pulseOx.js +28 -0
- package/dist/component/garmin/schemas/pulseOx.js.map +1 -0
- package/dist/component/garmin/schemas/respiration.d.ts +34 -0
- package/dist/component/garmin/schemas/respiration.d.ts.map +1 -0
- package/dist/component/garmin/schemas/respiration.js +28 -0
- package/dist/component/garmin/schemas/respiration.js.map +1 -0
- package/dist/component/garmin/schemas/skinTemperature.d.ts +36 -0
- package/dist/component/garmin/schemas/skinTemperature.d.ts.map +1 -0
- package/dist/component/garmin/schemas/skinTemperature.js +28 -0
- package/dist/component/garmin/schemas/skinTemperature.js.map +1 -0
- package/dist/component/garmin/schemas/sleeps.d.ts +88 -0
- package/dist/component/garmin/schemas/sleeps.d.ts.map +1 -0
- package/dist/component/garmin/schemas/sleeps.js +27 -0
- package/dist/component/garmin/schemas/sleeps.js.map +1 -0
- package/dist/component/garmin/schemas/stress.d.ts +70 -0
- package/dist/component/garmin/schemas/stress.d.ts.map +1 -0
- package/dist/component/garmin/schemas/stress.js +28 -0
- package/dist/component/garmin/schemas/stress.js.map +1 -0
- package/dist/component/garmin/schemas/userMetrics.d.ts +36 -0
- package/dist/component/garmin/schemas/userMetrics.d.ts.map +1 -0
- package/dist/component/garmin/schemas/userMetrics.js +28 -0
- package/dist/component/garmin/schemas/userMetrics.js.map +1 -0
- package/dist/component/garmin/transform/activity.d.ts +13 -0
- package/dist/component/garmin/transform/activity.d.ts.map +1 -0
- package/dist/component/garmin/transform/activity.js +111 -0
- package/dist/component/garmin/transform/activity.js.map +1 -0
- package/dist/component/garmin/transform/activityDetails.d.ts +13 -0
- package/dist/component/garmin/transform/activityDetails.d.ts.map +1 -0
- package/dist/component/garmin/transform/activityDetails.js +173 -0
- package/dist/component/garmin/transform/activityDetails.js.map +1 -0
- package/dist/component/garmin/transform/bloodPressure.d.ts +12 -0
- package/dist/component/garmin/transform/bloodPressure.d.ts.map +1 -0
- package/dist/component/garmin/transform/bloodPressure.js +33 -0
- package/dist/component/garmin/transform/bloodPressure.js.map +1 -0
- package/dist/component/garmin/transform/bodyCompositions.d.ts +12 -0
- package/dist/component/garmin/transform/bodyCompositions.d.ts.map +1 -0
- package/dist/component/garmin/transform/bodyCompositions.js +42 -0
- package/dist/component/garmin/transform/bodyCompositions.js.map +1 -0
- package/dist/component/garmin/transform/dailies.d.ts +12 -0
- package/dist/component/garmin/transform/dailies.d.ts.map +1 -0
- package/dist/component/garmin/transform/dailies.js +132 -0
- package/dist/component/garmin/transform/dailies.js.map +1 -0
- package/dist/component/garmin/transform/epochs.d.ts +13 -0
- package/dist/component/garmin/transform/epochs.d.ts.map +1 -0
- package/dist/component/garmin/transform/epochs.js +76 -0
- package/dist/component/garmin/transform/epochs.js.map +1 -0
- package/dist/component/garmin/transform/healthSnapshot.d.ts +12 -0
- package/dist/component/garmin/transform/healthSnapshot.d.ts.map +1 -0
- package/dist/component/garmin/transform/healthSnapshot.js +111 -0
- package/dist/component/garmin/transform/healthSnapshot.js.map +1 -0
- package/dist/component/garmin/transform/hrvSummary.d.ts +12 -0
- package/dist/component/garmin/transform/hrvSummary.d.ts.map +1 -0
- package/dist/component/garmin/transform/hrvSummary.js +45 -0
- package/dist/component/garmin/transform/hrvSummary.js.map +1 -0
- package/dist/component/garmin/transform/manuallyUpdatedActivities.d.ts +11 -0
- package/dist/component/garmin/transform/manuallyUpdatedActivities.d.ts.map +1 -0
- package/dist/component/garmin/transform/manuallyUpdatedActivities.js +20 -0
- package/dist/component/garmin/transform/manuallyUpdatedActivities.js.map +1 -0
- package/dist/component/garmin/transform/menstrualCycleTracking.d.ts +10 -0
- package/dist/component/garmin/transform/menstrualCycleTracking.d.ts.map +1 -0
- package/dist/component/garmin/transform/menstrualCycleTracking.js +43 -0
- package/dist/component/garmin/transform/menstrualCycleTracking.js.map +1 -0
- package/dist/component/garmin/transform/moveIQ.d.ts +17 -0
- package/dist/component/garmin/transform/moveIQ.d.ts.map +1 -0
- package/dist/component/garmin/transform/moveIQ.js +41 -0
- package/dist/component/garmin/transform/moveIQ.js.map +1 -0
- package/dist/component/garmin/transform/pulseOx.d.ts +12 -0
- package/dist/component/garmin/transform/pulseOx.d.ts.map +1 -0
- package/dist/component/garmin/transform/pulseOx.js +46 -0
- package/dist/component/garmin/transform/pulseOx.js.map +1 -0
- package/dist/component/garmin/transform/respiration.d.ts +12 -0
- package/dist/component/garmin/transform/respiration.d.ts.map +1 -0
- package/dist/component/garmin/transform/respiration.js +54 -0
- package/dist/component/garmin/transform/respiration.js.map +1 -0
- package/dist/component/garmin/transform/skinTemperature.d.ts +12 -0
- package/dist/component/garmin/transform/skinTemperature.d.ts.map +1 -0
- package/dist/component/garmin/transform/skinTemperature.js +38 -0
- package/dist/component/garmin/transform/skinTemperature.js.map +1 -0
- package/dist/component/garmin/transform/sleeps.d.ts +55 -0
- package/dist/component/garmin/transform/sleeps.d.ts.map +1 -0
- package/dist/component/garmin/transform/sleeps.js +120 -0
- package/dist/component/garmin/transform/sleeps.js.map +1 -0
- package/dist/component/garmin/transform/stress.d.ts +12 -0
- package/dist/component/garmin/transform/stress.d.ts.map +1 -0
- package/dist/component/garmin/transform/stress.js +56 -0
- package/dist/component/garmin/transform/stress.js.map +1 -0
- package/dist/component/garmin/transform/userMetrics.d.ts +12 -0
- package/dist/component/garmin/transform/userMetrics.d.ts.map +1 -0
- package/dist/component/garmin/transform/userMetrics.js +48 -0
- package/dist/component/garmin/transform/userMetrics.js.map +1 -0
- package/dist/component/garmin/types/garmin.d.ts +21 -0
- package/dist/component/garmin/types/garmin.d.ts.map +1 -0
- package/dist/component/garmin/types/garmin.js +6 -0
- package/dist/component/garmin/types/garmin.js.map +1 -0
- package/dist/component/garmin/types/zod/zod.gen.d.ts +1319 -0
- package/dist/component/garmin/types/zod/zod.gen.d.ts.map +1 -0
- package/dist/component/garmin/types/zod/zod.gen.js +784 -0
- package/dist/component/garmin/types/zod/zod.gen.js.map +1 -0
- package/dist/component/garmin/webhooks.d.ts +141 -0
- package/dist/component/garmin/webhooks.d.ts.map +1 -0
- package/dist/component/garmin/webhooks.js +766 -0
- package/dist/component/garmin/webhooks.js.map +1 -0
- package/dist/component/private.d.ts +20 -2
- package/dist/component/private.d.ts.map +1 -1
- package/dist/component/private.js +18 -0
- package/dist/component/private.js.map +1 -1
- package/dist/component/public.d.ts +433 -387
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +12 -2
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +217 -162
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +2 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava/private.d.ts +30 -0
- package/dist/component/strava/private.d.ts.map +1 -0
- package/dist/component/strava/private.js +71 -0
- package/dist/component/strava/private.js.map +1 -0
- package/dist/component/{strava.d.ts → strava/public.d.ts} +3 -31
- package/dist/component/strava/public.d.ts.map +1 -0
- package/dist/component/{strava.js → strava/public.js} +22 -101
- package/dist/component/strava/public.js.map +1 -0
- package/dist/component/validators/activity.d.ts +6 -0
- package/dist/component/validators/activity.d.ts.map +1 -1
- package/dist/component/validators/activity.js.map +1 -1
- package/dist/component/validators/body.d.ts +20 -14
- package/dist/component/validators/body.d.ts.map +1 -1
- package/dist/component/validators/body.js.map +1 -1
- package/dist/component/validators/connection.d.ts +1 -0
- package/dist/component/validators/connection.d.ts.map +1 -1
- package/dist/component/validators/connection.js +2 -0
- package/dist/component/validators/connection.js.map +1 -1
- package/dist/component/validators/daily.d.ts +46 -5
- package/dist/component/validators/daily.d.ts.map +1 -1
- package/dist/component/validators/daily.js +10 -1
- package/dist/component/validators/daily.js.map +1 -1
- package/dist/component/validators/enums.d.ts +2 -2
- package/dist/component/validators/menstruation.d.ts +5 -0
- package/dist/component/validators/menstruation.d.ts.map +1 -1
- package/dist/component/validators/menstruation.js.map +1 -1
- package/dist/component/validators/plannedWorkout.d.ts +5 -1
- package/dist/component/validators/plannedWorkout.d.ts.map +1 -1
- package/dist/component/validators/plannedWorkout.js +4 -0
- package/dist/component/validators/plannedWorkout.js.map +1 -1
- package/dist/component/validators/sleep.d.ts +8 -8
- package/dist/garmin/bloodPressure.d.ts +28 -0
- package/dist/garmin/bloodPressure.d.ts.map +1 -0
- package/dist/garmin/bloodPressure.js +34 -0
- package/dist/garmin/bloodPressure.js.map +1 -0
- package/dist/garmin/body.js +1 -1
- package/dist/garmin/body.js.map +1 -1
- package/dist/garmin/client.d.ts +117 -17
- package/dist/garmin/client.d.ts.map +1 -1
- package/dist/garmin/client.js +337 -43
- package/dist/garmin/client.js.map +1 -1
- package/dist/garmin/daily.d.ts.map +1 -1
- package/dist/garmin/daily.js +3 -3
- package/dist/garmin/daily.js.map +1 -1
- package/dist/garmin/hrv.d.ts +30 -0
- package/dist/garmin/hrv.d.ts.map +1 -0
- package/dist/garmin/hrv.js +45 -0
- package/dist/garmin/hrv.js.map +1 -0
- package/dist/garmin/index.d.ts +16 -4
- package/dist/garmin/index.d.ts.map +1 -1
- package/dist/garmin/index.js +8 -2
- package/dist/garmin/index.js.map +1 -1
- package/dist/garmin/maps/activity-type.d.ts +1 -2
- package/dist/garmin/maps/activity-type.d.ts.map +1 -1
- package/dist/garmin/maps/activity-type.js +1 -0
- package/dist/garmin/maps/activity-type.js.map +1 -1
- package/dist/garmin/menstruation.d.ts +6 -4
- package/dist/garmin/menstruation.d.ts.map +1 -1
- package/dist/garmin/menstruation.js +12 -8
- package/dist/garmin/menstruation.js.map +1 -1
- package/dist/garmin/pulseOx.d.ts +24 -0
- package/dist/garmin/pulseOx.d.ts.map +1 -0
- package/dist/garmin/pulseOx.js +33 -0
- package/dist/garmin/pulseOx.js.map +1 -0
- package/dist/garmin/respiration.d.ts +29 -0
- package/dist/garmin/respiration.d.ts.map +1 -0
- package/dist/garmin/respiration.js +42 -0
- package/dist/garmin/respiration.js.map +1 -0
- package/dist/garmin/skinTemp.d.ts +27 -0
- package/dist/garmin/skinTemp.d.ts.map +1 -0
- package/dist/garmin/skinTemp.js +35 -0
- package/dist/garmin/skinTemp.js.map +1 -0
- package/dist/garmin/sleep.d.ts +4 -4
- package/dist/garmin/sleep.d.ts.map +1 -1
- package/dist/garmin/sleep.js +15 -9
- package/dist/garmin/sleep.js.map +1 -1
- package/dist/garmin/stressDetails.d.ts +30 -0
- package/dist/garmin/stressDetails.d.ts.map +1 -0
- package/dist/garmin/stressDetails.js +49 -0
- package/dist/garmin/stressDetails.js.map +1 -0
- package/dist/garmin/sync.d.ts +14 -0
- package/dist/garmin/sync.d.ts.map +1 -1
- package/dist/garmin/sync.js +290 -7
- package/dist/garmin/sync.js.map +1 -1
- package/dist/garmin/types.d.ts +77 -186
- package/dist/garmin/types.d.ts.map +1 -1
- package/dist/garmin/types.js +4 -2
- package/dist/garmin/types.js.map +1 -1
- package/dist/garmin/userMetrics.d.ts +23 -0
- package/dist/garmin/userMetrics.d.ts.map +1 -0
- package/dist/garmin/userMetrics.js +41 -0
- package/dist/garmin/userMetrics.js.map +1 -0
- package/dist/validators.d.ts +138 -56
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +2 -2
- package/dist/validators.js.map +1 -1
- package/package.json +130 -124
- package/src/client/index.ts +86 -18
- package/src/component/_generated/api.ts +96 -4
- package/src/component/_generated/component.ts +271 -144
- package/src/{garmin → component/garmin}/auth.ts +8 -1
- package/src/component/garmin/client.ts +39 -0
- package/src/component/garmin/private.ts +1798 -0
- package/src/component/garmin/public.ts +938 -0
- package/src/component/garmin/schemas/activity.ts +40 -0
- package/src/component/garmin/schemas/activityDetails.ts +45 -0
- package/src/component/garmin/schemas/bloodPressure.ts +38 -0
- package/src/component/garmin/schemas/bodyCompositions.ts +38 -0
- package/src/component/garmin/schemas/dailies.ts +38 -0
- package/src/component/garmin/schemas/epochs.ts +38 -0
- package/src/component/garmin/schemas/healthSnapshot.ts +38 -0
- package/src/component/garmin/schemas/hrvSummary.ts +38 -0
- package/src/component/garmin/schemas/manuallyUpdatedActivities.ts +49 -0
- package/src/component/garmin/schemas/menstrualCycleTracking.ts +39 -0
- package/src/component/garmin/schemas/moveIQ.ts +38 -0
- package/src/component/garmin/schemas/pulseOx.ts +39 -0
- package/src/component/garmin/schemas/respiration.ts +39 -0
- package/src/component/garmin/schemas/skinTemperature.ts +39 -0
- package/src/component/garmin/schemas/sleeps.ts +38 -0
- package/src/component/garmin/schemas/stress.ts +43 -0
- package/src/component/garmin/schemas/userMetrics.ts +39 -0
- package/src/component/garmin/transform/activity.ts +143 -0
- package/src/component/garmin/transform/activityDetails.ts +236 -0
- package/src/component/garmin/transform/bloodPressure.ts +39 -0
- package/src/component/garmin/transform/bodyCompositions.ts +51 -0
- package/src/component/garmin/transform/dailies.ts +179 -0
- package/src/component/garmin/transform/epochs.ts +94 -0
- package/src/component/garmin/transform/healthSnapshot.ts +152 -0
- package/src/component/garmin/transform/hrvSummary.ts +56 -0
- package/src/component/garmin/transform/manuallyUpdatedActivities.ts +27 -0
- package/src/{garmin/maps/activity-type.ts → component/garmin/transform/maps/activityType.ts} +116 -116
- package/src/{garmin/maps/sleep-level.ts → component/garmin/transform/maps/sleepLevel.ts} +22 -22
- package/src/component/garmin/transform/menstrualCycleTracking.ts +48 -0
- package/src/component/garmin/transform/moveIQ.ts +48 -0
- package/src/{garmin → component/garmin/transform}/plannedWorkout.ts +328 -333
- package/src/component/garmin/transform/pulseOx.ts +64 -0
- package/src/component/garmin/transform/respiration.ts +73 -0
- package/src/component/garmin/transform/skinTemperature.ts +44 -0
- package/src/component/garmin/transform/sleeps.ts +159 -0
- package/src/component/garmin/transform/stress.ts +78 -0
- package/src/component/garmin/transform/userMetrics.ts +56 -0
- package/src/component/garmin/types/specs/training-api-workouts.json +699 -0
- package/src/component/garmin/types/specs/wellness-api.json +1 -0
- package/src/component/garmin/types/trainingApiWorkouts/client/client.gen.ts +290 -0
- package/src/component/garmin/types/trainingApiWorkouts/client/index.ts +25 -0
- package/src/component/garmin/types/trainingApiWorkouts/client/types.gen.ts +214 -0
- package/src/component/garmin/types/trainingApiWorkouts/client/utils.gen.ts +316 -0
- package/src/component/garmin/types/trainingApiWorkouts/client.gen.ts +16 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/auth.gen.ts +41 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/bodySerializer.gen.ts +82 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/params.gen.ts +169 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/pathSerializer.gen.ts +171 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/queryKeySerializer.gen.ts +117 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/serverSentEvents.gen.ts +243 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/types.gen.ts +104 -0
- package/src/component/garmin/types/trainingApiWorkouts/core/utils.gen.ts +140 -0
- package/src/component/garmin/types/trainingApiWorkouts/index.ts +4 -0
- package/src/component/garmin/types/trainingApiWorkouts/sdk.gen.ts +126 -0
- package/src/component/garmin/types/trainingApiWorkouts/types.gen.ts +387 -0
- package/src/component/garmin/types/trainingApiWorkouts/zod.gen.ts +423 -0
- package/src/component/garmin/types/wellnessApi/client/client.gen.ts +290 -0
- package/src/component/garmin/types/wellnessApi/client/index.ts +25 -0
- package/src/component/garmin/types/wellnessApi/client/types.gen.ts +214 -0
- package/src/component/garmin/types/wellnessApi/client/utils.gen.ts +316 -0
- package/src/component/garmin/types/wellnessApi/client.gen.ts +16 -0
- package/src/component/garmin/types/wellnessApi/core/auth.gen.ts +41 -0
- package/src/component/garmin/types/wellnessApi/core/bodySerializer.gen.ts +82 -0
- package/src/component/garmin/types/wellnessApi/core/params.gen.ts +169 -0
- package/src/component/garmin/types/wellnessApi/core/pathSerializer.gen.ts +171 -0
- package/src/component/garmin/types/wellnessApi/core/queryKeySerializer.gen.ts +117 -0
- package/src/component/garmin/types/wellnessApi/core/serverSentEvents.gen.ts +243 -0
- package/src/component/garmin/types/wellnessApi/core/types.gen.ts +104 -0
- package/src/component/garmin/types/wellnessApi/core/utils.gen.ts +140 -0
- package/src/component/garmin/types/wellnessApi/index.ts +4 -0
- package/src/component/garmin/types/wellnessApi/sdk.gen.ts +207 -0
- package/src/component/garmin/types/wellnessApi/types.gen.ts +2942 -0
- package/src/component/garmin/types/wellnessApi/zod.gen.ts +878 -0
- package/src/component/garmin/utils.ts +25 -0
- package/src/component/garmin/webhooks.ts +852 -0
- package/src/component/private.ts +21 -0
- package/src/component/public.ts +11 -2
- package/src/component/schema.ts +2 -1
- package/src/component/strava/private.ts +89 -0
- package/src/component/{strava.ts → strava/public.ts} +341 -404
- package/src/component/validators/activity.ts +5 -0
- package/src/component/validators/body.ts +5 -0
- package/src/component/validators/connection.ts +2 -0
- package/src/component/validators/daily.ts +20 -0
- package/src/component/validators/menstruation.ts +5 -1
- package/src/component/validators/plannedWorkout.ts +9 -0
- package/src/validators.ts +12 -2
- package/dist/component/garmin.d.ts.map +0 -1
- package/dist/component/garmin.js.map +0 -1
- package/dist/component/strava.d.ts.map +0 -1
- package/dist/component/strava.js.map +0 -1
- package/dist/garmin/activity.d.ts +0 -101
- package/dist/garmin/activity.d.ts.map +0 -1
- package/dist/garmin/activity.js +0 -207
- package/dist/garmin/activity.js.map +0 -1
- package/src/component/garmin.ts +0 -850
- package/src/garmin/activity.test.ts +0 -178
- package/src/garmin/activity.ts +0 -272
- package/src/garmin/auth.test.ts +0 -103
- package/src/garmin/body.ts +0 -59
- package/src/garmin/client.ts +0 -407
- package/src/garmin/daily.ts +0 -211
- package/src/garmin/index.ts +0 -75
- package/src/garmin/maps/activity-type.test.ts +0 -78
- package/src/garmin/menstruation.ts +0 -42
- package/src/garmin/sleep.test.ts +0 -110
- package/src/garmin/sleep.ts +0 -170
- package/src/garmin/sync.ts +0 -223
- package/src/garmin/types.ts +0 -480
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
// ─── Garmin Internal Mutations ───────────────────────────────────────────────
|
|
2
|
+
// Token CRUD and pending OAuth state management.
|
|
3
|
+
// Called only from garmin/public.ts and garmin/webhooks.ts.
|
|
4
|
+
|
|
5
|
+
import { v } from "convex/values";
|
|
6
|
+
import {
|
|
7
|
+
internalAction,
|
|
8
|
+
internalMutation,
|
|
9
|
+
internalQuery,
|
|
10
|
+
} from "../_generated/server";
|
|
11
|
+
import { internal } from "../_generated/api";
|
|
12
|
+
import {
|
|
13
|
+
garminActivityPingPayloadSchema,
|
|
14
|
+
garminActivityPushPayloadSchema,
|
|
15
|
+
} from "./schemas/activity.js";
|
|
16
|
+
import {
|
|
17
|
+
garminActivityDetailsPingPayloadSchema,
|
|
18
|
+
garminActivityDetailsPushPayloadSchema,
|
|
19
|
+
} from "./schemas/activityDetails.js";
|
|
20
|
+
import {
|
|
21
|
+
garminManuallyUpdatedActivitiesPingPayloadSchema,
|
|
22
|
+
garminManuallyUpdatedActivitiesPushPayloadSchema,
|
|
23
|
+
} from "./schemas/manuallyUpdatedActivities.js";
|
|
24
|
+
import {
|
|
25
|
+
garminMoveIQPingPayloadSchema,
|
|
26
|
+
garminMoveIQPushPayloadSchema,
|
|
27
|
+
} from "./schemas/moveIQ.js";
|
|
28
|
+
import {
|
|
29
|
+
garminBloodPressurePingPayloadSchema,
|
|
30
|
+
garminBloodPressurePushPayloadSchema,
|
|
31
|
+
} from "./schemas/bloodPressure.js";
|
|
32
|
+
import {
|
|
33
|
+
garminBodyCompositionsPingPayloadSchema,
|
|
34
|
+
garminBodyCompositionsPushPayloadSchema,
|
|
35
|
+
} from "./schemas/bodyCompositions.js";
|
|
36
|
+
import {
|
|
37
|
+
garminDailiesPingPayloadSchema,
|
|
38
|
+
garminDailiesPushPayloadSchema,
|
|
39
|
+
} from "./schemas/dailies.js";
|
|
40
|
+
import {
|
|
41
|
+
garminHealthSnapshotPingPayloadSchema,
|
|
42
|
+
garminHealthSnapshotPushPayloadSchema,
|
|
43
|
+
} from "./schemas/healthSnapshot.js";
|
|
44
|
+
import {
|
|
45
|
+
garminHRVSummaryPingPayloadSchema,
|
|
46
|
+
garminHRVSummaryPushPayloadSchema,
|
|
47
|
+
} from "./schemas/hrvSummary.js";
|
|
48
|
+
import {
|
|
49
|
+
garminEpochPingPayloadSchema,
|
|
50
|
+
garminEpochPushPayloadSchema,
|
|
51
|
+
} from "./schemas/epochs.js";
|
|
52
|
+
import {
|
|
53
|
+
garminPulseOxPingPayloadSchema,
|
|
54
|
+
garminPulseOxPushPayloadSchema,
|
|
55
|
+
} from "./schemas/pulseOx.js";
|
|
56
|
+
import {
|
|
57
|
+
garminRespirationPingPayloadSchema,
|
|
58
|
+
garminRespirationPushPayloadSchema,
|
|
59
|
+
} from "./schemas/respiration.js";
|
|
60
|
+
import {
|
|
61
|
+
garminSkinTemperaturePingPayloadSchema,
|
|
62
|
+
garminSkinTemperaturePushPayloadSchema,
|
|
63
|
+
} from "./schemas/skinTemperature.js";
|
|
64
|
+
import {
|
|
65
|
+
garminStressPingPayloadSchema,
|
|
66
|
+
garminStressPushPayloadSchema,
|
|
67
|
+
} from "./schemas/stress.js";
|
|
68
|
+
import {
|
|
69
|
+
garminSleepsPingPayloadSchema,
|
|
70
|
+
garminSleepsPushPayloadSchema,
|
|
71
|
+
} from "./schemas/sleeps.js";
|
|
72
|
+
import { transformActivity } from "./transform/activity.js";
|
|
73
|
+
import { transformActivityDetails } from "./transform/activityDetails.js";
|
|
74
|
+
import { transformManuallyUpdatedActivity } from "./transform/manuallyUpdatedActivities.js";
|
|
75
|
+
import { transformMoveIQ } from "./transform/moveIQ.js";
|
|
76
|
+
import { transformBloodPressure } from "./transform/bloodPressure.js";
|
|
77
|
+
import { transformBodyComposition } from "./transform/bodyCompositions.js";
|
|
78
|
+
import { transformDailies } from "./transform/dailies.js";
|
|
79
|
+
import { transformEpoch } from "./transform/epochs.js";
|
|
80
|
+
import { transformHealthSnapshot } from "./transform/healthSnapshot.js";
|
|
81
|
+
import { transformHRVSummary } from "./transform/hrvSummary.js";
|
|
82
|
+
import { transformPulseOx } from "./transform/pulseOx.js";
|
|
83
|
+
import { transformRespiration } from "./transform/respiration.js";
|
|
84
|
+
import { transformSkinTemperature } from "./transform/skinTemperature.js";
|
|
85
|
+
import { transformSleeps } from "./transform/sleeps.js";
|
|
86
|
+
import { transformStress } from "./transform/stress.js";
|
|
87
|
+
import {
|
|
88
|
+
garminUserMetricsPingPayloadSchema,
|
|
89
|
+
garminUserMetricsPushPayloadSchema,
|
|
90
|
+
} from "./schemas/userMetrics.js";
|
|
91
|
+
import { transformUserMetrics } from "./transform/userMetrics.js";
|
|
92
|
+
import {
|
|
93
|
+
garminMenstrualCycleTrackingPingPayloadSchema,
|
|
94
|
+
garminMenstrualCycleTrackingPushPayloadSchema,
|
|
95
|
+
} from "./schemas/menstrualCycleTracking.js";
|
|
96
|
+
import { transformMenstrualCycleTracking } from "./transform/menstrualCycleTracking.js";
|
|
97
|
+
// ─── Internal Pending OAuth CRUD ─────────────────────────────────────────────
|
|
98
|
+
// Temporary storage for in-progress Garmin OAuth 2.0 PKCE flows.
|
|
99
|
+
// Bridges getGarminAuthUrl and completeGarminOAuth.
|
|
100
|
+
|
|
101
|
+
export const storePendingOAuth = internalMutation({
|
|
102
|
+
args: {
|
|
103
|
+
provider: v.string(),
|
|
104
|
+
state: v.string(),
|
|
105
|
+
codeVerifier: v.string(),
|
|
106
|
+
userId: v.string(),
|
|
107
|
+
},
|
|
108
|
+
returns: v.null(),
|
|
109
|
+
handler: async (ctx, args) => {
|
|
110
|
+
await ctx.db.insert("pendingOAuth", {
|
|
111
|
+
...args,
|
|
112
|
+
createdAt: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
return null;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const getPendingOAuth = internalQuery({
|
|
119
|
+
args: { state: v.string() },
|
|
120
|
+
returns: v.union(
|
|
121
|
+
v.object({
|
|
122
|
+
_id: v.id("pendingOAuth"),
|
|
123
|
+
_creationTime: v.number(),
|
|
124
|
+
provider: v.string(),
|
|
125
|
+
state: v.string(),
|
|
126
|
+
codeVerifier: v.string(),
|
|
127
|
+
userId: v.string(),
|
|
128
|
+
createdAt: v.number(),
|
|
129
|
+
}),
|
|
130
|
+
v.null(),
|
|
131
|
+
),
|
|
132
|
+
handler: async (ctx, args) => {
|
|
133
|
+
return await ctx.db
|
|
134
|
+
.query("pendingOAuth")
|
|
135
|
+
.withIndex("by_state", (q) => q.eq("state", args.state))
|
|
136
|
+
.first();
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const deletePendingOAuth = internalMutation({
|
|
141
|
+
args: { state: v.string() },
|
|
142
|
+
returns: v.null(),
|
|
143
|
+
handler: async (ctx, args) => {
|
|
144
|
+
const pending = await ctx.db
|
|
145
|
+
.query("pendingOAuth")
|
|
146
|
+
.withIndex("by_state", (q) => q.eq("state", args.state))
|
|
147
|
+
.first();
|
|
148
|
+
if (pending) {
|
|
149
|
+
await ctx.db.delete(pending._id);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── Internal Token CRUD ─────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Store OAuth 2.0 tokens for a Garmin connection.
|
|
159
|
+
* Upserts by connectionId — one token record per connection.
|
|
160
|
+
*/
|
|
161
|
+
export const storeTokens = internalMutation({
|
|
162
|
+
args: {
|
|
163
|
+
connectionId: v.id("connections"),
|
|
164
|
+
accessToken: v.string(),
|
|
165
|
+
refreshToken: v.string(),
|
|
166
|
+
expiresAt: v.number(),
|
|
167
|
+
},
|
|
168
|
+
returns: v.null(),
|
|
169
|
+
handler: async (ctx, args) => {
|
|
170
|
+
const existing = await ctx.db
|
|
171
|
+
.query("providerTokens")
|
|
172
|
+
.withIndex("by_connectionId", (q) =>
|
|
173
|
+
q.eq("connectionId", args.connectionId),
|
|
174
|
+
)
|
|
175
|
+
.first();
|
|
176
|
+
|
|
177
|
+
if (existing) {
|
|
178
|
+
await ctx.db.patch(existing._id, {
|
|
179
|
+
accessToken: args.accessToken,
|
|
180
|
+
refreshToken: args.refreshToken,
|
|
181
|
+
expiresAt: args.expiresAt,
|
|
182
|
+
});
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await ctx.db.insert("providerTokens", {
|
|
187
|
+
connectionId: args.connectionId,
|
|
188
|
+
accessToken: args.accessToken,
|
|
189
|
+
refreshToken: args.refreshToken,
|
|
190
|
+
expiresAt: args.expiresAt,
|
|
191
|
+
});
|
|
192
|
+
return null;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get stored tokens for a connection.
|
|
198
|
+
*/
|
|
199
|
+
export const getTokens = internalQuery({
|
|
200
|
+
args: { connectionId: v.id("connections") },
|
|
201
|
+
returns: v.union(
|
|
202
|
+
v.object({
|
|
203
|
+
_id: v.id("providerTokens"),
|
|
204
|
+
_creationTime: v.number(),
|
|
205
|
+
connectionId: v.id("connections"),
|
|
206
|
+
accessToken: v.string(),
|
|
207
|
+
refreshToken: v.optional(v.string()),
|
|
208
|
+
expiresAt: v.optional(v.number()),
|
|
209
|
+
}),
|
|
210
|
+
v.null(),
|
|
211
|
+
),
|
|
212
|
+
handler: async (ctx, args) => {
|
|
213
|
+
return await ctx.db
|
|
214
|
+
.query("providerTokens")
|
|
215
|
+
.withIndex("by_connectionId", (q) =>
|
|
216
|
+
q.eq("connectionId", args.connectionId),
|
|
217
|
+
)
|
|
218
|
+
.first();
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Delete stored tokens for a connection.
|
|
224
|
+
*/
|
|
225
|
+
export const deleteTokens = internalMutation({
|
|
226
|
+
args: { connectionId: v.id("connections") },
|
|
227
|
+
returns: v.null(),
|
|
228
|
+
handler: async (ctx, args) => {
|
|
229
|
+
const existing = await ctx.db
|
|
230
|
+
.query("providerTokens")
|
|
231
|
+
.withIndex("by_connectionId", (q) =>
|
|
232
|
+
q.eq("connectionId", args.connectionId),
|
|
233
|
+
)
|
|
234
|
+
.first();
|
|
235
|
+
|
|
236
|
+
if (existing) {
|
|
237
|
+
await ctx.db.delete(existing._id);
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ─── Activity Push Processing ───────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Process a Garmin activity push payload.
|
|
247
|
+
* Parses the full activity data, groups by user, resolves connections,
|
|
248
|
+
* transforms, and ingests each activity.
|
|
249
|
+
*/
|
|
250
|
+
export const processActivityPushPayload = internalAction({
|
|
251
|
+
args: { payload: v.any() },
|
|
252
|
+
handler: async (ctx, args) => {
|
|
253
|
+
const { activities } = garminActivityPushPayloadSchema.parse(args.payload);
|
|
254
|
+
|
|
255
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
256
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
257
|
+
|
|
258
|
+
// Group items by Garmin userId
|
|
259
|
+
const byUser = new Map<string, typeof activities>();
|
|
260
|
+
for (const item of activities) {
|
|
261
|
+
const existing = byUser.get(item.userId);
|
|
262
|
+
if (existing) {
|
|
263
|
+
existing.push(item);
|
|
264
|
+
} else {
|
|
265
|
+
byUser.set(item.userId, [item]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
270
|
+
const connection = await ctx.runQuery(
|
|
271
|
+
internal.private.getConnectionByProviderUserId,
|
|
272
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (!connection) {
|
|
276
|
+
for (const item of userItems) {
|
|
277
|
+
errors.push({
|
|
278
|
+
type: "activity",
|
|
279
|
+
id: item.summaryId ?? "unknown",
|
|
280
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!connection.active) {
|
|
287
|
+
for (const item of userItems) {
|
|
288
|
+
errors.push({
|
|
289
|
+
type: "activity",
|
|
290
|
+
id: item.summaryId ?? "unknown",
|
|
291
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const item of userItems) {
|
|
298
|
+
try {
|
|
299
|
+
const data = transformActivity(item);
|
|
300
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
errors.push({
|
|
303
|
+
type: "activity",
|
|
304
|
+
id: item.summaryId ?? "unknown",
|
|
305
|
+
error: err instanceof Error ? err.message : String(err),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { items, errors };
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ─── Activity Ping Processing ──────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Process a Garmin activity ping payload.
|
|
319
|
+
* Stub — acknowledges the notification without fetching data.
|
|
320
|
+
*/
|
|
321
|
+
export const processActivityPingPayload = internalAction({
|
|
322
|
+
args: { payload: v.any() },
|
|
323
|
+
handler: async (_ctx, args) => {
|
|
324
|
+
const { activities } = garminActivityPingPayloadSchema.parse(args.payload);
|
|
325
|
+
console.log(
|
|
326
|
+
`[garmin:webhook:activities] Ping mode not yet implemented, acknowledging ${activities.length} items`,
|
|
327
|
+
);
|
|
328
|
+
return { processed: 0, errors: [] };
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ─── Activity Details Push Processing ─────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Process a Garmin activity details push payload.
|
|
336
|
+
* Parses the full activity detail data (summary + samples + laps),
|
|
337
|
+
* groups by user, resolves connections, transforms, and ingests each activity.
|
|
338
|
+
*/
|
|
339
|
+
export const processActivityDetailsPushPayload = internalAction({
|
|
340
|
+
args: { payload: v.any() },
|
|
341
|
+
handler: async (ctx, args) => {
|
|
342
|
+
const { activityDetails } =
|
|
343
|
+
garminActivityDetailsPushPayloadSchema.parse(args.payload);
|
|
344
|
+
|
|
345
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
346
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
347
|
+
|
|
348
|
+
// Group items by Garmin userId
|
|
349
|
+
const byUser = new Map<string, typeof activityDetails>();
|
|
350
|
+
for (const item of activityDetails) {
|
|
351
|
+
const existing = byUser.get(item.userId);
|
|
352
|
+
if (existing) {
|
|
353
|
+
existing.push(item);
|
|
354
|
+
} else {
|
|
355
|
+
byUser.set(item.userId, [item]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
360
|
+
const connection = await ctx.runQuery(
|
|
361
|
+
internal.private.getConnectionByProviderUserId,
|
|
362
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (!connection) {
|
|
366
|
+
for (const item of userItems) {
|
|
367
|
+
errors.push({
|
|
368
|
+
type: "activityDetails",
|
|
369
|
+
id: item.summaryId ?? "unknown",
|
|
370
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!connection.active) {
|
|
377
|
+
for (const item of userItems) {
|
|
378
|
+
errors.push({
|
|
379
|
+
type: "activityDetails",
|
|
380
|
+
id: item.summaryId ?? "unknown",
|
|
381
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const item of userItems) {
|
|
388
|
+
try {
|
|
389
|
+
const data = transformActivityDetails(item);
|
|
390
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
391
|
+
} catch (err) {
|
|
392
|
+
errors.push({
|
|
393
|
+
type: "activityDetails",
|
|
394
|
+
id: item.summaryId ?? "unknown",
|
|
395
|
+
error: err instanceof Error ? err.message : String(err),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { items, errors };
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ─── Activity Details Ping Processing ─────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Process a Garmin activity details ping payload.
|
|
409
|
+
* Stub — acknowledges the notification without fetching data.
|
|
410
|
+
*/
|
|
411
|
+
export const processActivityDetailsPingPayload = internalAction({
|
|
412
|
+
args: { payload: v.any() },
|
|
413
|
+
handler: async (_ctx, args) => {
|
|
414
|
+
const { activityDetails } =
|
|
415
|
+
garminActivityDetailsPingPayloadSchema.parse(args.payload);
|
|
416
|
+
console.log(
|
|
417
|
+
`[garmin:webhook:activityDetails] Ping mode not yet implemented, acknowledging ${activityDetails.length} items`,
|
|
418
|
+
);
|
|
419
|
+
return { processed: 0, errors: [] };
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ─── Manually Updated Activities Push Processing ────────────────────────────
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Process a Garmin manually updated activities push payload.
|
|
427
|
+
* Parses the full activity data, groups by user, resolves connections,
|
|
428
|
+
* transforms, and ingests each activity.
|
|
429
|
+
*/
|
|
430
|
+
export const processManuallyUpdatedActivitiesPushPayload = internalAction({
|
|
431
|
+
args: { payload: v.any() },
|
|
432
|
+
handler: async (ctx, args) => {
|
|
433
|
+
const { manuallyUpdatedActivities } =
|
|
434
|
+
garminManuallyUpdatedActivitiesPushPayloadSchema.parse(args.payload);
|
|
435
|
+
|
|
436
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
437
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
438
|
+
|
|
439
|
+
// Group items by Garmin userId
|
|
440
|
+
const byUser = new Map<string, typeof manuallyUpdatedActivities>();
|
|
441
|
+
for (const item of manuallyUpdatedActivities) {
|
|
442
|
+
const existing = byUser.get(item.userId);
|
|
443
|
+
if (existing) {
|
|
444
|
+
existing.push(item);
|
|
445
|
+
} else {
|
|
446
|
+
byUser.set(item.userId, [item]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
451
|
+
const connection = await ctx.runQuery(
|
|
452
|
+
internal.private.getConnectionByProviderUserId,
|
|
453
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (!connection) {
|
|
457
|
+
for (const item of userItems) {
|
|
458
|
+
errors.push({
|
|
459
|
+
type: "manuallyUpdatedActivities",
|
|
460
|
+
id: item.summaryId ?? "unknown",
|
|
461
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!connection.active) {
|
|
468
|
+
for (const item of userItems) {
|
|
469
|
+
errors.push({
|
|
470
|
+
type: "manuallyUpdatedActivities",
|
|
471
|
+
id: item.summaryId ?? "unknown",
|
|
472
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const item of userItems) {
|
|
479
|
+
try {
|
|
480
|
+
const data = transformManuallyUpdatedActivity(item);
|
|
481
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
482
|
+
} catch (err) {
|
|
483
|
+
errors.push({
|
|
484
|
+
type: "manuallyUpdatedActivities",
|
|
485
|
+
id: item.summaryId ?? "unknown",
|
|
486
|
+
error: err instanceof Error ? err.message : String(err),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return { items, errors };
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ─── Manually Updated Activities Ping Processing ────────────────────────────
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Process a Garmin manually updated activities ping payload.
|
|
500
|
+
* Stub — acknowledges the notification without fetching data.
|
|
501
|
+
*/
|
|
502
|
+
export const processManuallyUpdatedActivitiesPingPayload = internalAction({
|
|
503
|
+
args: { payload: v.any() },
|
|
504
|
+
handler: async (_ctx, args) => {
|
|
505
|
+
const { manuallyUpdatedActivities } =
|
|
506
|
+
garminManuallyUpdatedActivitiesPingPayloadSchema.parse(args.payload);
|
|
507
|
+
console.log(
|
|
508
|
+
`[garmin:webhook:manuallyUpdatedActivities] Ping mode not yet implemented, acknowledging ${manuallyUpdatedActivities.length} items`,
|
|
509
|
+
);
|
|
510
|
+
return { processed: 0, errors: [] };
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ─── Move IQ Push Processing ──────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Process a Garmin Move IQ push payload.
|
|
518
|
+
* Parses the auto-detected activity events, groups by user, resolves
|
|
519
|
+
* connections, transforms, and ingests each event as an activity.
|
|
520
|
+
*/
|
|
521
|
+
export const processMoveIQPushPayload = internalAction({
|
|
522
|
+
args: { payload: v.any() },
|
|
523
|
+
handler: async (ctx, args) => {
|
|
524
|
+
const { moveIQActivities } = garminMoveIQPushPayloadSchema.parse(
|
|
525
|
+
args.payload,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
529
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
530
|
+
|
|
531
|
+
// Group items by Garmin userId
|
|
532
|
+
const byUser = new Map<string, typeof moveIQActivities>();
|
|
533
|
+
for (const item of moveIQActivities) {
|
|
534
|
+
const existing = byUser.get(item.userId);
|
|
535
|
+
if (existing) {
|
|
536
|
+
existing.push(item);
|
|
537
|
+
} else {
|
|
538
|
+
byUser.set(item.userId, [item]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
543
|
+
const connection = await ctx.runQuery(
|
|
544
|
+
internal.private.getConnectionByProviderUserId,
|
|
545
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
if (!connection) {
|
|
549
|
+
for (const item of userItems) {
|
|
550
|
+
errors.push({
|
|
551
|
+
type: "moveIQ",
|
|
552
|
+
id: item.summaryId ?? "unknown",
|
|
553
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!connection.active) {
|
|
560
|
+
for (const item of userItems) {
|
|
561
|
+
errors.push({
|
|
562
|
+
type: "moveIQ",
|
|
563
|
+
id: item.summaryId ?? "unknown",
|
|
564
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
for (const item of userItems) {
|
|
571
|
+
try {
|
|
572
|
+
const data = transformMoveIQ(item);
|
|
573
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
574
|
+
} catch (err) {
|
|
575
|
+
errors.push({
|
|
576
|
+
type: "moveIQ",
|
|
577
|
+
id: item.summaryId ?? "unknown",
|
|
578
|
+
error: err instanceof Error ? err.message : String(err),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return { items, errors };
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// ─── Move IQ Ping Processing ─────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Process a Garmin Move IQ ping payload.
|
|
592
|
+
* Stub — acknowledges the notification without fetching data.
|
|
593
|
+
*/
|
|
594
|
+
export const processMoveIQPingPayload = internalAction({
|
|
595
|
+
args: { payload: v.any() },
|
|
596
|
+
handler: async (_ctx, args) => {
|
|
597
|
+
const { moveIQActivities } = garminMoveIQPingPayloadSchema.parse(
|
|
598
|
+
args.payload,
|
|
599
|
+
);
|
|
600
|
+
console.log(
|
|
601
|
+
`[garmin:webhook:moveIQ] Ping mode not yet implemented, acknowledging ${moveIQActivities.length} items`,
|
|
602
|
+
);
|
|
603
|
+
return { processed: 0, errors: [] };
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// ─── Blood Pressure Push Processing ─────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Process a Garmin blood pressure push payload.
|
|
611
|
+
* Parses the full blood pressure data, groups by user, resolves connections,
|
|
612
|
+
* transforms, and ingests each record into the body table.
|
|
613
|
+
*/
|
|
614
|
+
export const processBloodPressurePushPayload = internalAction({
|
|
615
|
+
args: { payload: v.any() },
|
|
616
|
+
handler: async (ctx, args) => {
|
|
617
|
+
const { bloodPressures } =
|
|
618
|
+
garminBloodPressurePushPayloadSchema.parse(args.payload);
|
|
619
|
+
|
|
620
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
621
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
622
|
+
|
|
623
|
+
// Group items by Garmin userId
|
|
624
|
+
const byUser = new Map<string, typeof bloodPressures>();
|
|
625
|
+
for (const item of bloodPressures) {
|
|
626
|
+
const existing = byUser.get(item.userId);
|
|
627
|
+
if (existing) {
|
|
628
|
+
existing.push(item);
|
|
629
|
+
} else {
|
|
630
|
+
byUser.set(item.userId, [item]);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
635
|
+
const connection = await ctx.runQuery(
|
|
636
|
+
internal.private.getConnectionByProviderUserId,
|
|
637
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
if (!connection) {
|
|
641
|
+
for (const item of userItems) {
|
|
642
|
+
errors.push({
|
|
643
|
+
type: "bloodPressure",
|
|
644
|
+
id: item.summaryId ?? "unknown",
|
|
645
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!connection.active) {
|
|
652
|
+
for (const item of userItems) {
|
|
653
|
+
errors.push({
|
|
654
|
+
type: "bloodPressure",
|
|
655
|
+
id: item.summaryId ?? "unknown",
|
|
656
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
for (const item of userItems) {
|
|
663
|
+
try {
|
|
664
|
+
const data = transformBloodPressure(item);
|
|
665
|
+
if (data == null) continue;
|
|
666
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
667
|
+
} catch (err) {
|
|
668
|
+
errors.push({
|
|
669
|
+
type: "bloodPressure",
|
|
670
|
+
id: item.summaryId ?? "unknown",
|
|
671
|
+
error: err instanceof Error ? err.message : String(err),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return { items, errors };
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// ─── Blood Pressure Ping Processing ─────────────────────────────────────────
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Process a Garmin blood pressure ping payload.
|
|
685
|
+
* Stub — acknowledges the notification without fetching data.
|
|
686
|
+
*/
|
|
687
|
+
export const processBloodPressurePingPayload = internalAction({
|
|
688
|
+
args: { payload: v.any() },
|
|
689
|
+
handler: async (_ctx, args) => {
|
|
690
|
+
const { bloodPressures } =
|
|
691
|
+
garminBloodPressurePingPayloadSchema.parse(args.payload);
|
|
692
|
+
console.log(
|
|
693
|
+
`[garmin:webhook:bloodPressures] Ping mode not yet implemented, acknowledging ${bloodPressures.length} items`,
|
|
694
|
+
);
|
|
695
|
+
return { processed: 0, errors: [] };
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// ─── Body Compositions Push Processing ─────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Process a Garmin body compositions push payload.
|
|
703
|
+
* Parses the full body composition data, groups by user, resolves connections,
|
|
704
|
+
* transforms, and ingests each record into the body table.
|
|
705
|
+
*/
|
|
706
|
+
export const processBodyCompositionsPushPayload = internalAction({
|
|
707
|
+
args: { payload: v.any() },
|
|
708
|
+
handler: async (ctx, args) => {
|
|
709
|
+
const { bodyComps } =
|
|
710
|
+
garminBodyCompositionsPushPayloadSchema.parse(args.payload);
|
|
711
|
+
|
|
712
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
713
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
714
|
+
|
|
715
|
+
// Group items by Garmin userId
|
|
716
|
+
const byUser = new Map<string, typeof bodyComps>();
|
|
717
|
+
for (const item of bodyComps) {
|
|
718
|
+
const existing = byUser.get(item.userId);
|
|
719
|
+
if (existing) {
|
|
720
|
+
existing.push(item);
|
|
721
|
+
} else {
|
|
722
|
+
byUser.set(item.userId, [item]);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
727
|
+
const connection = await ctx.runQuery(
|
|
728
|
+
internal.private.getConnectionByProviderUserId,
|
|
729
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
if (!connection) {
|
|
733
|
+
for (const item of userItems) {
|
|
734
|
+
errors.push({
|
|
735
|
+
type: "bodyCompositions",
|
|
736
|
+
id: item.summaryId ?? "unknown",
|
|
737
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!connection.active) {
|
|
744
|
+
for (const item of userItems) {
|
|
745
|
+
errors.push({
|
|
746
|
+
type: "bodyCompositions",
|
|
747
|
+
id: item.summaryId ?? "unknown",
|
|
748
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for (const item of userItems) {
|
|
755
|
+
try {
|
|
756
|
+
const data = transformBodyComposition(item);
|
|
757
|
+
if (data == null) continue;
|
|
758
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
759
|
+
} catch (err) {
|
|
760
|
+
errors.push({
|
|
761
|
+
type: "bodyCompositions",
|
|
762
|
+
id: item.summaryId ?? "unknown",
|
|
763
|
+
error: err instanceof Error ? err.message : String(err),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return { items, errors };
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// ─── Body Compositions Ping Processing ─────────────────────────────────────
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Process a Garmin body compositions ping payload.
|
|
777
|
+
* Stub — acknowledges the notification without fetching data.
|
|
778
|
+
*/
|
|
779
|
+
export const processBodyCompositionsPingPayload = internalAction({
|
|
780
|
+
args: { payload: v.any() },
|
|
781
|
+
handler: async (_ctx, args) => {
|
|
782
|
+
const { bodyComps } =
|
|
783
|
+
garminBodyCompositionsPingPayloadSchema.parse(args.payload);
|
|
784
|
+
console.log(
|
|
785
|
+
`[garmin:webhook:bodyCompositions] Ping mode not yet implemented, acknowledging ${bodyComps.length} items`,
|
|
786
|
+
);
|
|
787
|
+
return { processed: 0, errors: [] };
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ─── Dailies Push Processing ──────────────────────────────────────────────
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Process a Garmin dailies push payload.
|
|
795
|
+
* Parses the full daily summary data, groups by user, resolves connections,
|
|
796
|
+
* transforms, and ingests each record into the daily table.
|
|
797
|
+
*/
|
|
798
|
+
export const processDailiesPushPayload = internalAction({
|
|
799
|
+
args: { payload: v.any() },
|
|
800
|
+
handler: async (ctx, args) => {
|
|
801
|
+
const { dailies } =
|
|
802
|
+
garminDailiesPushPayloadSchema.parse(args.payload);
|
|
803
|
+
|
|
804
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
805
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
806
|
+
|
|
807
|
+
// Group items by Garmin userId
|
|
808
|
+
const byUser = new Map<string, typeof dailies>();
|
|
809
|
+
for (const item of dailies) {
|
|
810
|
+
const existing = byUser.get(item.userId);
|
|
811
|
+
if (existing) {
|
|
812
|
+
existing.push(item);
|
|
813
|
+
} else {
|
|
814
|
+
byUser.set(item.userId, [item]);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
819
|
+
const connection = await ctx.runQuery(
|
|
820
|
+
internal.private.getConnectionByProviderUserId,
|
|
821
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
if (!connection) {
|
|
825
|
+
for (const item of userItems) {
|
|
826
|
+
errors.push({
|
|
827
|
+
type: "dailies",
|
|
828
|
+
id: item.summaryId ?? "unknown",
|
|
829
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (!connection.active) {
|
|
836
|
+
for (const item of userItems) {
|
|
837
|
+
errors.push({
|
|
838
|
+
type: "dailies",
|
|
839
|
+
id: item.summaryId ?? "unknown",
|
|
840
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const item of userItems) {
|
|
847
|
+
try {
|
|
848
|
+
const data = transformDailies(item);
|
|
849
|
+
if (data == null) continue;
|
|
850
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
851
|
+
} catch (err) {
|
|
852
|
+
errors.push({
|
|
853
|
+
type: "dailies",
|
|
854
|
+
id: item.summaryId ?? "unknown",
|
|
855
|
+
error: err instanceof Error ? err.message : String(err),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return { items, errors };
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// ─── Dailies Ping Processing ──────────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Process a Garmin dailies ping payload.
|
|
869
|
+
* Stub — acknowledges the notification without fetching data.
|
|
870
|
+
*/
|
|
871
|
+
export const processDailiesPingPayload = internalAction({
|
|
872
|
+
args: { payload: v.any() },
|
|
873
|
+
handler: async (_ctx, args) => {
|
|
874
|
+
const { dailies } =
|
|
875
|
+
garminDailiesPingPayloadSchema.parse(args.payload);
|
|
876
|
+
console.log(
|
|
877
|
+
`[garmin:webhook:dailies] Ping mode not yet implemented, acknowledging ${dailies.length} items`,
|
|
878
|
+
);
|
|
879
|
+
return { processed: 0, errors: [] };
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// ─── Health Snapshot Push Processing ────────────────────────────────────────
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Process a Garmin health snapshot push payload.
|
|
887
|
+
* Parses the full health snapshot data, groups by user, resolves connections,
|
|
888
|
+
* transforms, and ingests each record into the daily table.
|
|
889
|
+
*/
|
|
890
|
+
export const processHealthSnapshotPushPayload = internalAction({
|
|
891
|
+
args: { payload: v.any() },
|
|
892
|
+
handler: async (ctx, args) => {
|
|
893
|
+
const { healthSnapshot } =
|
|
894
|
+
garminHealthSnapshotPushPayloadSchema.parse(args.payload);
|
|
895
|
+
|
|
896
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
897
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
898
|
+
|
|
899
|
+
// Group items by Garmin userId
|
|
900
|
+
const byUser = new Map<string, typeof healthSnapshot>();
|
|
901
|
+
for (const item of healthSnapshot) {
|
|
902
|
+
const existing = byUser.get(item.userId);
|
|
903
|
+
if (existing) {
|
|
904
|
+
existing.push(item);
|
|
905
|
+
} else {
|
|
906
|
+
byUser.set(item.userId, [item]);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
911
|
+
const connection = await ctx.runQuery(
|
|
912
|
+
internal.private.getConnectionByProviderUserId,
|
|
913
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
if (!connection) {
|
|
917
|
+
for (const item of userItems) {
|
|
918
|
+
errors.push({
|
|
919
|
+
type: "healthSnapshot",
|
|
920
|
+
id: item.summaryId ?? "unknown",
|
|
921
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!connection.active) {
|
|
928
|
+
for (const item of userItems) {
|
|
929
|
+
errors.push({
|
|
930
|
+
type: "healthSnapshot",
|
|
931
|
+
id: item.summaryId ?? "unknown",
|
|
932
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
for (const item of userItems) {
|
|
939
|
+
try {
|
|
940
|
+
const data = transformHealthSnapshot(item);
|
|
941
|
+
if (data == null) continue;
|
|
942
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
943
|
+
} catch (err) {
|
|
944
|
+
errors.push({
|
|
945
|
+
type: "healthSnapshot",
|
|
946
|
+
id: item.summaryId ?? "unknown",
|
|
947
|
+
error: err instanceof Error ? err.message : String(err),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
return { items, errors };
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// ─── Health Snapshot Ping Processing ────────────────────────────────────────
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Process a Garmin health snapshot ping payload.
|
|
961
|
+
* Stub — acknowledges the notification without fetching data.
|
|
962
|
+
*/
|
|
963
|
+
export const processHealthSnapshotPingPayload = internalAction({
|
|
964
|
+
args: { payload: v.any() },
|
|
965
|
+
handler: async (_ctx, args) => {
|
|
966
|
+
const { healthSnapshot } =
|
|
967
|
+
garminHealthSnapshotPingPayloadSchema.parse(args.payload);
|
|
968
|
+
console.log(
|
|
969
|
+
`[garmin:webhook:healthSnapshot] Ping mode not yet implemented, acknowledging ${healthSnapshot.length} items`,
|
|
970
|
+
);
|
|
971
|
+
return { processed: 0, errors: [] };
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// ─── HRV Summary Push Processing ────────────────────────────────────────────
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Process a Garmin HRV summary push payload.
|
|
979
|
+
* Parses the full HRV summary data, groups by user, resolves connections,
|
|
980
|
+
* transforms, and ingests each record into the daily table.
|
|
981
|
+
*/
|
|
982
|
+
export const processHRVSummaryPushPayload = internalAction({
|
|
983
|
+
args: { payload: v.any() },
|
|
984
|
+
handler: async (ctx, args) => {
|
|
985
|
+
const { hrv } =
|
|
986
|
+
garminHRVSummaryPushPayloadSchema.parse(args.payload);
|
|
987
|
+
|
|
988
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
989
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
990
|
+
|
|
991
|
+
// Group items by Garmin userId
|
|
992
|
+
const byUser = new Map<string, typeof hrv>();
|
|
993
|
+
for (const item of hrv) {
|
|
994
|
+
const existing = byUser.get(item.userId);
|
|
995
|
+
if (existing) {
|
|
996
|
+
existing.push(item);
|
|
997
|
+
} else {
|
|
998
|
+
byUser.set(item.userId, [item]);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1003
|
+
const connection = await ctx.runQuery(
|
|
1004
|
+
internal.private.getConnectionByProviderUserId,
|
|
1005
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
if (!connection) {
|
|
1009
|
+
for (const item of userItems) {
|
|
1010
|
+
errors.push({
|
|
1011
|
+
type: "hrvSummary",
|
|
1012
|
+
id: item.summaryId ?? "unknown",
|
|
1013
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (!connection.active) {
|
|
1020
|
+
for (const item of userItems) {
|
|
1021
|
+
errors.push({
|
|
1022
|
+
type: "hrvSummary",
|
|
1023
|
+
id: item.summaryId ?? "unknown",
|
|
1024
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
for (const item of userItems) {
|
|
1031
|
+
try {
|
|
1032
|
+
const data = transformHRVSummary(item);
|
|
1033
|
+
if (data == null) continue;
|
|
1034
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
errors.push({
|
|
1037
|
+
type: "hrvSummary",
|
|
1038
|
+
id: item.summaryId ?? "unknown",
|
|
1039
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return { items, errors };
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// ─── HRV Summary Ping Processing ────────────────────────────────────────────
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Process a Garmin HRV summary ping payload.
|
|
1053
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1054
|
+
*/
|
|
1055
|
+
export const processHRVSummaryPingPayload = internalAction({
|
|
1056
|
+
args: { payload: v.any() },
|
|
1057
|
+
handler: async (_ctx, args) => {
|
|
1058
|
+
const { hrv } =
|
|
1059
|
+
garminHRVSummaryPingPayloadSchema.parse(args.payload);
|
|
1060
|
+
console.log(
|
|
1061
|
+
`[garmin:webhook:hrvSummary] Ping mode not yet implemented, acknowledging ${hrv.length} items`,
|
|
1062
|
+
);
|
|
1063
|
+
return { processed: 0, errors: [] };
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// ─── Epoch Push Processing ──────────────────────────────────────────────────
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Process a Garmin epoch push payload.
|
|
1071
|
+
* Parses the full epoch summary data (15-min wellness slices), groups by user,
|
|
1072
|
+
* resolves connections, transforms, and ingests each record into the daily table.
|
|
1073
|
+
*/
|
|
1074
|
+
export const processEpochPushPayload = internalAction({
|
|
1075
|
+
args: { payload: v.any() },
|
|
1076
|
+
handler: async (ctx, args) => {
|
|
1077
|
+
const { epochs } =
|
|
1078
|
+
garminEpochPushPayloadSchema.parse(args.payload);
|
|
1079
|
+
|
|
1080
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1081
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1082
|
+
|
|
1083
|
+
// Group items by Garmin userId
|
|
1084
|
+
const byUser = new Map<string, typeof epochs>();
|
|
1085
|
+
for (const item of epochs) {
|
|
1086
|
+
const existing = byUser.get(item.userId);
|
|
1087
|
+
if (existing) {
|
|
1088
|
+
existing.push(item);
|
|
1089
|
+
} else {
|
|
1090
|
+
byUser.set(item.userId, [item]);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1095
|
+
const connection = await ctx.runQuery(
|
|
1096
|
+
internal.private.getConnectionByProviderUserId,
|
|
1097
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
if (!connection) {
|
|
1101
|
+
for (const item of userItems) {
|
|
1102
|
+
errors.push({
|
|
1103
|
+
type: "epochs",
|
|
1104
|
+
id: item.summaryId ?? "unknown",
|
|
1105
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (!connection.active) {
|
|
1112
|
+
for (const item of userItems) {
|
|
1113
|
+
errors.push({
|
|
1114
|
+
type: "epochs",
|
|
1115
|
+
id: item.summaryId ?? "unknown",
|
|
1116
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
for (const item of userItems) {
|
|
1123
|
+
try {
|
|
1124
|
+
const data = transformEpoch(item);
|
|
1125
|
+
if (data == null) continue;
|
|
1126
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
errors.push({
|
|
1129
|
+
type: "epochs",
|
|
1130
|
+
id: item.summaryId ?? "unknown",
|
|
1131
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return { items, errors };
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// ─── Epoch Ping Processing ──────────────────────────────────────────────────
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Process a Garmin epoch ping payload.
|
|
1145
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1146
|
+
*/
|
|
1147
|
+
export const processEpochPingPayload = internalAction({
|
|
1148
|
+
args: { payload: v.any() },
|
|
1149
|
+
handler: async (_ctx, args) => {
|
|
1150
|
+
const { epochs } =
|
|
1151
|
+
garminEpochPingPayloadSchema.parse(args.payload);
|
|
1152
|
+
console.log(
|
|
1153
|
+
`[garmin:webhook:epochs] Ping mode not yet implemented, acknowledging ${epochs.length} items`,
|
|
1154
|
+
);
|
|
1155
|
+
return { processed: 0, errors: [] };
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
// ─── Pulse Ox Push Processing ──────────────────────────────────────────────
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Process a Garmin Pulse Ox push payload.
|
|
1163
|
+
* Parses the full SpO2 data, groups by user, resolves connections,
|
|
1164
|
+
* transforms, and ingests each record into the daily table.
|
|
1165
|
+
*/
|
|
1166
|
+
export const processPulseOxPushPayload = internalAction({
|
|
1167
|
+
args: { payload: v.any() },
|
|
1168
|
+
handler: async (ctx, args) => {
|
|
1169
|
+
const { pulseox } =
|
|
1170
|
+
garminPulseOxPushPayloadSchema.parse(args.payload);
|
|
1171
|
+
|
|
1172
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1173
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1174
|
+
|
|
1175
|
+
// Group items by Garmin userId
|
|
1176
|
+
const byUser = new Map<string, typeof pulseox>();
|
|
1177
|
+
for (const item of pulseox) {
|
|
1178
|
+
const existing = byUser.get(item.userId);
|
|
1179
|
+
if (existing) {
|
|
1180
|
+
existing.push(item);
|
|
1181
|
+
} else {
|
|
1182
|
+
byUser.set(item.userId, [item]);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1187
|
+
const connection = await ctx.runQuery(
|
|
1188
|
+
internal.private.getConnectionByProviderUserId,
|
|
1189
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
if (!connection) {
|
|
1193
|
+
for (const item of userItems) {
|
|
1194
|
+
errors.push({
|
|
1195
|
+
type: "pulseOx",
|
|
1196
|
+
id: item.summaryId ?? "unknown",
|
|
1197
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (!connection.active) {
|
|
1204
|
+
for (const item of userItems) {
|
|
1205
|
+
errors.push({
|
|
1206
|
+
type: "pulseOx",
|
|
1207
|
+
id: item.summaryId ?? "unknown",
|
|
1208
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
for (const item of userItems) {
|
|
1215
|
+
try {
|
|
1216
|
+
const data = transformPulseOx(item);
|
|
1217
|
+
if (data == null) continue;
|
|
1218
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
errors.push({
|
|
1221
|
+
type: "pulseOx",
|
|
1222
|
+
id: item.summaryId ?? "unknown",
|
|
1223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return { items, errors };
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
// ─── Pulse Ox Ping Processing ──────────────────────────────────────────────
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Process a Garmin Pulse Ox ping payload.
|
|
1237
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1238
|
+
*/
|
|
1239
|
+
export const processPulseOxPingPayload = internalAction({
|
|
1240
|
+
args: { payload: v.any() },
|
|
1241
|
+
handler: async (_ctx, args) => {
|
|
1242
|
+
const { pulseox } =
|
|
1243
|
+
garminPulseOxPingPayloadSchema.parse(args.payload);
|
|
1244
|
+
console.log(
|
|
1245
|
+
`[garmin:webhook:pulseOx] Ping mode not yet implemented, acknowledging ${pulseox.length} items`,
|
|
1246
|
+
);
|
|
1247
|
+
return { processed: 0, errors: [] };
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// ─── Respiration Push Processing ──────────────────────────────────────────
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Process a Garmin Respiration push payload.
|
|
1255
|
+
* Parses the full breathing rate data, groups by user, resolves connections,
|
|
1256
|
+
* transforms, and ingests each record into the daily table.
|
|
1257
|
+
*/
|
|
1258
|
+
export const processRespirationPushPayload = internalAction({
|
|
1259
|
+
args: { payload: v.any() },
|
|
1260
|
+
handler: async (ctx, args) => {
|
|
1261
|
+
const { allDayRespiration } =
|
|
1262
|
+
garminRespirationPushPayloadSchema.parse(args.payload);
|
|
1263
|
+
|
|
1264
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1265
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1266
|
+
|
|
1267
|
+
// Group items by Garmin userId
|
|
1268
|
+
const byUser = new Map<string, typeof allDayRespiration>();
|
|
1269
|
+
for (const item of allDayRespiration) {
|
|
1270
|
+
const existing = byUser.get(item.userId);
|
|
1271
|
+
if (existing) {
|
|
1272
|
+
existing.push(item);
|
|
1273
|
+
} else {
|
|
1274
|
+
byUser.set(item.userId, [item]);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1279
|
+
const connection = await ctx.runQuery(
|
|
1280
|
+
internal.private.getConnectionByProviderUserId,
|
|
1281
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
if (!connection) {
|
|
1285
|
+
for (const item of userItems) {
|
|
1286
|
+
errors.push({
|
|
1287
|
+
type: "respiration",
|
|
1288
|
+
id: item.summaryId ?? "unknown",
|
|
1289
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (!connection.active) {
|
|
1296
|
+
for (const item of userItems) {
|
|
1297
|
+
errors.push({
|
|
1298
|
+
type: "respiration",
|
|
1299
|
+
id: item.summaryId ?? "unknown",
|
|
1300
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
for (const item of userItems) {
|
|
1307
|
+
try {
|
|
1308
|
+
const data = transformRespiration(item);
|
|
1309
|
+
if (data == null) continue;
|
|
1310
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
errors.push({
|
|
1313
|
+
type: "respiration",
|
|
1314
|
+
id: item.summaryId ?? "unknown",
|
|
1315
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
return { items, errors };
|
|
1322
|
+
},
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// ─── Respiration Ping Processing ──────────────────────────────────────────
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Process a Garmin Respiration ping payload.
|
|
1329
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1330
|
+
*/
|
|
1331
|
+
export const processRespirationPingPayload = internalAction({
|
|
1332
|
+
args: { payload: v.any() },
|
|
1333
|
+
handler: async (_ctx, args) => {
|
|
1334
|
+
const { allDayRespiration } =
|
|
1335
|
+
garminRespirationPingPayloadSchema.parse(args.payload);
|
|
1336
|
+
console.log(
|
|
1337
|
+
`[garmin:webhook:respiration] Ping mode not yet implemented, acknowledging ${allDayRespiration.length} items`,
|
|
1338
|
+
);
|
|
1339
|
+
return { processed: 0, errors: [] };
|
|
1340
|
+
},
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// ─── Stress Details Push Processing ──────────────────────────────────────────
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Process a Garmin stress details push payload.
|
|
1347
|
+
* Parses the full stress data, groups by user, resolves connections,
|
|
1348
|
+
* transforms, and ingests each record into the daily table.
|
|
1349
|
+
*/
|
|
1350
|
+
export const processStressPushPayload = internalAction({
|
|
1351
|
+
args: { payload: v.any() },
|
|
1352
|
+
handler: async (ctx, args) => {
|
|
1353
|
+
const { stressDetails } =
|
|
1354
|
+
garminStressPushPayloadSchema.parse(args.payload);
|
|
1355
|
+
|
|
1356
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1357
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1358
|
+
|
|
1359
|
+
// Group items by Garmin userId
|
|
1360
|
+
const byUser = new Map<string, typeof stressDetails>();
|
|
1361
|
+
for (const item of stressDetails) {
|
|
1362
|
+
const existing = byUser.get(item.userId);
|
|
1363
|
+
if (existing) {
|
|
1364
|
+
existing.push(item);
|
|
1365
|
+
} else {
|
|
1366
|
+
byUser.set(item.userId, [item]);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1371
|
+
const connection = await ctx.runQuery(
|
|
1372
|
+
internal.private.getConnectionByProviderUserId,
|
|
1373
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
if (!connection) {
|
|
1377
|
+
for (const item of userItems) {
|
|
1378
|
+
errors.push({
|
|
1379
|
+
type: "stressDetails",
|
|
1380
|
+
id: item.summaryId ?? "unknown",
|
|
1381
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (!connection.active) {
|
|
1388
|
+
for (const item of userItems) {
|
|
1389
|
+
errors.push({
|
|
1390
|
+
type: "stressDetails",
|
|
1391
|
+
id: item.summaryId ?? "unknown",
|
|
1392
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
for (const item of userItems) {
|
|
1399
|
+
try {
|
|
1400
|
+
const data = transformStress(item);
|
|
1401
|
+
if (data == null) continue;
|
|
1402
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
errors.push({
|
|
1405
|
+
type: "stressDetails",
|
|
1406
|
+
id: item.summaryId ?? "unknown",
|
|
1407
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
return { items, errors };
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// ─── Stress Details Ping Processing ─────────────────────────────────────────
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Process a Garmin stress details ping payload.
|
|
1421
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1422
|
+
*/
|
|
1423
|
+
export const processStressPingPayload = internalAction({
|
|
1424
|
+
args: { payload: v.any() },
|
|
1425
|
+
handler: async (_ctx, args) => {
|
|
1426
|
+
const { stressDetails } =
|
|
1427
|
+
garminStressPingPayloadSchema.parse(args.payload);
|
|
1428
|
+
console.log(
|
|
1429
|
+
`[garmin:webhook:stressDetails] Ping mode not yet implemented, acknowledging ${stressDetails.length} items`,
|
|
1430
|
+
);
|
|
1431
|
+
return { processed: 0, errors: [] };
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
// ─── Skin Temperature Push Processing ───────────────────────────────────────
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Process a Garmin skin temperature push payload.
|
|
1439
|
+
* Parses the full skin temperature data, groups by user, resolves connections,
|
|
1440
|
+
* transforms, and ingests each record into the body table.
|
|
1441
|
+
*/
|
|
1442
|
+
export const processSkinTemperaturePushPayload = internalAction({
|
|
1443
|
+
args: { payload: v.any() },
|
|
1444
|
+
handler: async (ctx, args) => {
|
|
1445
|
+
const { skinTemp } =
|
|
1446
|
+
garminSkinTemperaturePushPayloadSchema.parse(args.payload);
|
|
1447
|
+
|
|
1448
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1449
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1450
|
+
|
|
1451
|
+
// Group items by Garmin userId
|
|
1452
|
+
const byUser = new Map<string, typeof skinTemp>();
|
|
1453
|
+
for (const item of skinTemp) {
|
|
1454
|
+
const existing = byUser.get(item.userId);
|
|
1455
|
+
if (existing) {
|
|
1456
|
+
existing.push(item);
|
|
1457
|
+
} else {
|
|
1458
|
+
byUser.set(item.userId, [item]);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1463
|
+
const connection = await ctx.runQuery(
|
|
1464
|
+
internal.private.getConnectionByProviderUserId,
|
|
1465
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
if (!connection) {
|
|
1469
|
+
for (const item of userItems) {
|
|
1470
|
+
errors.push({
|
|
1471
|
+
type: "skinTemperature",
|
|
1472
|
+
id: item.summaryId ?? "unknown",
|
|
1473
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (!connection.active) {
|
|
1480
|
+
for (const item of userItems) {
|
|
1481
|
+
errors.push({
|
|
1482
|
+
type: "skinTemperature",
|
|
1483
|
+
id: item.summaryId ?? "unknown",
|
|
1484
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
for (const item of userItems) {
|
|
1491
|
+
try {
|
|
1492
|
+
const data = transformSkinTemperature(item);
|
|
1493
|
+
if (data == null) continue;
|
|
1494
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
errors.push({
|
|
1497
|
+
type: "skinTemperature",
|
|
1498
|
+
id: item.summaryId ?? "unknown",
|
|
1499
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
return { items, errors };
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
// ─── Skin Temperature Ping Processing ───────────────────────────────────────
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Process a Garmin skin temperature ping payload.
|
|
1513
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1514
|
+
*/
|
|
1515
|
+
export const processSkinTemperaturePingPayload = internalAction({
|
|
1516
|
+
args: { payload: v.any() },
|
|
1517
|
+
handler: async (_ctx, args) => {
|
|
1518
|
+
const { skinTemp } =
|
|
1519
|
+
garminSkinTemperaturePingPayloadSchema.parse(args.payload);
|
|
1520
|
+
console.log(
|
|
1521
|
+
`[garmin:webhook:skinTemperature] Ping mode not yet implemented, acknowledging ${skinTemp.length} items`,
|
|
1522
|
+
);
|
|
1523
|
+
return { processed: 0, errors: [] };
|
|
1524
|
+
},
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// ─── Sleeps Push Processing ─────────────────────────────────────────────────
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Process a Garmin sleep summary push payload.
|
|
1531
|
+
* Parses the full sleep data, groups by user, resolves connections,
|
|
1532
|
+
* transforms, and ingests each sleep record.
|
|
1533
|
+
*/
|
|
1534
|
+
export const processSleepsPushPayload = internalAction({
|
|
1535
|
+
args: { payload: v.any() },
|
|
1536
|
+
handler: async (ctx, args) => {
|
|
1537
|
+
const { sleeps } = garminSleepsPushPayloadSchema.parse(args.payload);
|
|
1538
|
+
|
|
1539
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1540
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1541
|
+
|
|
1542
|
+
// Group items by Garmin userId
|
|
1543
|
+
const byUser = new Map<string, typeof sleeps>();
|
|
1544
|
+
for (const item of sleeps) {
|
|
1545
|
+
const existing = byUser.get(item.userId);
|
|
1546
|
+
if (existing) {
|
|
1547
|
+
existing.push(item);
|
|
1548
|
+
} else {
|
|
1549
|
+
byUser.set(item.userId, [item]);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1554
|
+
const connection = await ctx.runQuery(
|
|
1555
|
+
internal.private.getConnectionByProviderUserId,
|
|
1556
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
if (!connection) {
|
|
1560
|
+
for (const item of userItems) {
|
|
1561
|
+
errors.push({
|
|
1562
|
+
type: "sleep",
|
|
1563
|
+
id: item.summaryId ?? "unknown",
|
|
1564
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (!connection.active) {
|
|
1571
|
+
for (const item of userItems) {
|
|
1572
|
+
errors.push({
|
|
1573
|
+
type: "sleep",
|
|
1574
|
+
id: item.summaryId ?? "unknown",
|
|
1575
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
for (const item of userItems) {
|
|
1582
|
+
try {
|
|
1583
|
+
const data = transformSleeps(item);
|
|
1584
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
errors.push({
|
|
1587
|
+
type: "sleep",
|
|
1588
|
+
id: item.summaryId ?? "unknown",
|
|
1589
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
return { items, errors };
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
// ─── Sleeps Ping Processing ─────────────────────────────────────────────────
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* Process a Garmin sleep summary ping payload.
|
|
1603
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1604
|
+
*/
|
|
1605
|
+
export const processSleepsPingPayload = internalAction({
|
|
1606
|
+
args: { payload: v.any() },
|
|
1607
|
+
handler: async (_ctx, args) => {
|
|
1608
|
+
const { sleeps } = garminSleepsPingPayloadSchema.parse(args.payload);
|
|
1609
|
+
console.log(
|
|
1610
|
+
`[garmin:webhook:sleeps] Ping mode not yet implemented, acknowledging ${sleeps.length} items`,
|
|
1611
|
+
);
|
|
1612
|
+
return { processed: 0, errors: [] };
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// ─── User Metrics Push Processing ──────────────────────────────────────────
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Process a Garmin user metrics push payload.
|
|
1620
|
+
* Parses the full user metrics data, groups by user, resolves connections,
|
|
1621
|
+
* transforms, and ingests each user metrics record.
|
|
1622
|
+
*/
|
|
1623
|
+
export const processUserMetricsPushPayload = internalAction({
|
|
1624
|
+
args: { payload: v.any() },
|
|
1625
|
+
handler: async (ctx, args) => {
|
|
1626
|
+
const { userMetrics } =
|
|
1627
|
+
garminUserMetricsPushPayloadSchema.parse(args.payload);
|
|
1628
|
+
|
|
1629
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1630
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1631
|
+
|
|
1632
|
+
// Group items by Garmin userId
|
|
1633
|
+
const byUser = new Map<string, typeof userMetrics>();
|
|
1634
|
+
for (const item of userMetrics) {
|
|
1635
|
+
const existing = byUser.get(item.userId);
|
|
1636
|
+
if (existing) {
|
|
1637
|
+
existing.push(item);
|
|
1638
|
+
} else {
|
|
1639
|
+
byUser.set(item.userId, [item]);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1644
|
+
const connection = await ctx.runQuery(
|
|
1645
|
+
internal.private.getConnectionByProviderUserId,
|
|
1646
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1647
|
+
);
|
|
1648
|
+
|
|
1649
|
+
if (!connection) {
|
|
1650
|
+
for (const item of userItems) {
|
|
1651
|
+
errors.push({
|
|
1652
|
+
type: "userMetrics",
|
|
1653
|
+
id: item.summaryId ?? "unknown",
|
|
1654
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (!connection.active) {
|
|
1661
|
+
for (const item of userItems) {
|
|
1662
|
+
errors.push({
|
|
1663
|
+
type: "userMetrics",
|
|
1664
|
+
id: item.summaryId ?? "unknown",
|
|
1665
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
for (const item of userItems) {
|
|
1672
|
+
try {
|
|
1673
|
+
const data = transformUserMetrics(item);
|
|
1674
|
+
if (data == null) continue;
|
|
1675
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
errors.push({
|
|
1678
|
+
type: "userMetrics",
|
|
1679
|
+
id: item.summaryId ?? "unknown",
|
|
1680
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return { items, errors };
|
|
1687
|
+
},
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// ─── User Metrics Ping Processing ──────────────────────────────────────────
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Process a Garmin user metrics ping payload.
|
|
1694
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1695
|
+
*/
|
|
1696
|
+
export const processUserMetricsPingPayload = internalAction({
|
|
1697
|
+
args: { payload: v.any() },
|
|
1698
|
+
handler: async (_ctx, args) => {
|
|
1699
|
+
const { userMetrics } =
|
|
1700
|
+
garminUserMetricsPingPayloadSchema.parse(args.payload);
|
|
1701
|
+
console.log(
|
|
1702
|
+
`[garmin:webhook:userMetrics] Ping mode not yet implemented, acknowledging ${userMetrics.length} items`,
|
|
1703
|
+
);
|
|
1704
|
+
return { processed: 0, errors: [] };
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// ─── Menstrual Cycle Tracking Push Processing ───────────────────────────────
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Process a Garmin MCT (Women's Health API) push payload.
|
|
1712
|
+
* Parses full MCT summary data, groups by user, resolves connections,
|
|
1713
|
+
* transforms, and ingests each record into the menstruation table.
|
|
1714
|
+
*/
|
|
1715
|
+
export const processMenstrualCycleTrackingPushPayload = internalAction({
|
|
1716
|
+
args: { payload: v.any() },
|
|
1717
|
+
handler: async (ctx, args) => {
|
|
1718
|
+
const { mct } =
|
|
1719
|
+
garminMenstrualCycleTrackingPushPayloadSchema.parse(args.payload);
|
|
1720
|
+
|
|
1721
|
+
const items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }> = [];
|
|
1722
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
1723
|
+
|
|
1724
|
+
// Group items by Garmin userId
|
|
1725
|
+
const byUser = new Map<string, typeof mct>();
|
|
1726
|
+
for (const item of mct) {
|
|
1727
|
+
const existing = byUser.get(item.userId);
|
|
1728
|
+
if (existing) {
|
|
1729
|
+
existing.push(item);
|
|
1730
|
+
} else {
|
|
1731
|
+
byUser.set(item.userId, [item]);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
for (const [garminUserId, userItems] of byUser) {
|
|
1736
|
+
const connection = await ctx.runQuery(
|
|
1737
|
+
internal.private.getConnectionByProviderUserId,
|
|
1738
|
+
{ providerUserId: garminUserId, provider: "GARMIN" },
|
|
1739
|
+
);
|
|
1740
|
+
|
|
1741
|
+
if (!connection) {
|
|
1742
|
+
for (const item of userItems) {
|
|
1743
|
+
errors.push({
|
|
1744
|
+
type: "menstrualCycleTracking",
|
|
1745
|
+
id: item.summaryId ?? "unknown",
|
|
1746
|
+
error: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (!connection.active) {
|
|
1753
|
+
for (const item of userItems) {
|
|
1754
|
+
errors.push({
|
|
1755
|
+
type: "menstrualCycleTracking",
|
|
1756
|
+
id: item.summaryId ?? "unknown",
|
|
1757
|
+
error: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
for (const item of userItems) {
|
|
1764
|
+
try {
|
|
1765
|
+
const data = transformMenstrualCycleTracking(item);
|
|
1766
|
+
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
errors.push({
|
|
1769
|
+
type: "menstrualCycleTracking",
|
|
1770
|
+
id: item.summaryId ?? "unknown",
|
|
1771
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return { items, errors };
|
|
1778
|
+
},
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
// ─── Menstrual Cycle Tracking Ping Processing ───────────────────────────────
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Process a Garmin MCT ping payload.
|
|
1785
|
+
* Stub — acknowledges the notification without fetching data.
|
|
1786
|
+
*/
|
|
1787
|
+
export const processMenstrualCycleTrackingPingPayload = internalAction({
|
|
1788
|
+
args: { payload: v.any() },
|
|
1789
|
+
handler: async (_ctx, args) => {
|
|
1790
|
+
const { mct } =
|
|
1791
|
+
garminMenstrualCycleTrackingPingPayloadSchema.parse(args.payload);
|
|
1792
|
+
console.log(
|
|
1793
|
+
`[garmin:webhook:menstrualCycleTracking] Ping mode not yet implemented, acknowledging ${mct.length} items`,
|
|
1794
|
+
);
|
|
1795
|
+
return { processed: 0, errors: [] };
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
|