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