@nativesquare/soma 0.12.0 → 0.13.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 (60) hide show
  1. package/dist/client/garmin.d.ts +5 -1
  2. package/dist/client/garmin.d.ts.map +1 -1
  3. package/dist/client/garmin.js +148 -0
  4. package/dist/client/garmin.js.map +1 -1
  5. package/dist/client/index.d.ts +5 -6
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +5 -211
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/strava.d.ts +11 -6
  10. package/dist/client/strava.d.ts.map +1 -1
  11. package/dist/client/strava.js +64 -0
  12. package/dist/client/strava.js.map +1 -1
  13. package/dist/client/types.d.ts +93 -20
  14. package/dist/client/types.d.ts.map +1 -1
  15. package/dist/component/_generated/component.d.ts +24 -5
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/garmin/private.d.ts +53 -68
  18. package/dist/component/garmin/private.d.ts.map +1 -1
  19. package/dist/component/garmin/private.js +87 -85
  20. package/dist/component/garmin/private.js.map +1 -1
  21. package/dist/component/garmin/public.d.ts +97 -43
  22. package/dist/component/garmin/public.d.ts.map +1 -1
  23. package/dist/component/garmin/public.js +75 -51
  24. package/dist/component/garmin/public.js.map +1 -1
  25. package/dist/component/garmin/webhooks.d.ts +22 -20
  26. package/dist/component/garmin/webhooks.d.ts.map +1 -1
  27. package/dist/component/garmin/webhooks.js +115 -76
  28. package/dist/component/garmin/webhooks.js.map +1 -1
  29. package/dist/component/public.d.ts +15 -15
  30. package/dist/component/schema.d.ts +25 -25
  31. package/dist/component/strava/public.d.ts +12 -8
  32. package/dist/component/strava/public.d.ts.map +1 -1
  33. package/dist/component/strava/public.js +7 -7
  34. package/dist/component/strava/public.js.map +1 -1
  35. package/dist/component/validators/activity.d.ts +4 -4
  36. package/dist/component/validators/body.d.ts +4 -4
  37. package/dist/component/validators/daily.d.ts +4 -4
  38. package/dist/component/validators/nutrition.d.ts +3 -3
  39. package/dist/component/validators/samples.d.ts +4 -4
  40. package/dist/component/validators/shared.d.ts +13 -4
  41. package/dist/component/validators/shared.d.ts.map +1 -1
  42. package/dist/component/validators/shared.js +7 -0
  43. package/dist/component/validators/shared.js.map +1 -1
  44. package/dist/component/validators/sleep.d.ts +5 -5
  45. package/dist/validators.d.ts +41 -40
  46. package/dist/validators.d.ts.map +1 -1
  47. package/dist/validators.js +1 -0
  48. package/dist/validators.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/client/garmin.ts +692 -487
  51. package/src/client/index.ts +10 -279
  52. package/src/client/strava.ts +199 -108
  53. package/src/client/types.ts +303 -215
  54. package/src/component/_generated/component.ts +19 -19
  55. package/src/component/garmin/private.ts +1872 -1870
  56. package/src/component/garmin/public.ts +104 -80
  57. package/src/component/garmin/webhooks.ts +122 -81
  58. package/src/component/strava/public.ts +393 -393
  59. package/src/component/validators/shared.ts +9 -0
  60. package/src/validators.ts +1 -0
@@ -103,17 +103,30 @@ function isWebhookPushMode(payload: unknown): boolean {
103
103
  return !("callbackURL" in firstItem);
104
104
  }
105
105
 
106
+ /** Shape returned by every public webhook handler action to the HTTP layer. */
106
107
  type WebhookResult = {
107
- processed: number;
108
- errors: Array<{ type: string; id: string; error: string }>;
109
- affectedUsers: Array<{ userId: string; connectionId: string }>;
108
+ errors: Array<{ type: string; id: string; message: string }>;
109
+ items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }>;
110
110
  };
111
111
 
112
+ /** Shape returned by internal push-processing actions (transform only, no DB writes). */
112
113
  type ProcessResult = {
113
114
  items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }>;
114
- errors: Array<{ type: string; id: string; error: string }>;
115
+ errors: Array<{ type: string; id: string; message: string }>;
115
116
  };
116
117
 
118
+ /**
119
+ * Build a WebhookResult from a ProcessResult without writing to the database.
120
+ * Used when `autoIngest` is disabled — the host app still gets the full set of
121
+ * transformed items in its callbacks, but no data is persisted.
122
+ */
123
+ function toWebhookResult(result: ProcessResult): WebhookResult {
124
+ return {
125
+ errors: result.errors,
126
+ items: result.items,
127
+ };
128
+ }
129
+
117
130
  /**
118
131
  * Ingest transformed items from a private handler and update connections.
119
132
  * Shared orchestration logic for all push-mode webhook handlers.
@@ -124,10 +137,8 @@ async function ingestAndUpdate(
124
137
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
138
  ingestMutation: any,
126
139
  ): Promise<WebhookResult> {
127
- let processed = 0;
128
140
  const errors = [...result.errors];
129
141
  const connectionsToUpdate = new Set<string>();
130
- const userMap = new Map<string, { userId: string; connectionId: string }>();
131
142
 
132
143
  for (const item of result.items) {
133
144
  try {
@@ -135,18 +146,13 @@ async function ingestAndUpdate(
135
146
  connectionId: item.connectionId,
136
147
  userId: item.userId,
137
148
  ...item.data,
138
- } as never);
139
- processed++;
140
- connectionsToUpdate.add(item.connectionId);
141
- userMap.set(`${item.userId}:${item.connectionId}`, {
142
- userId: item.userId,
143
- connectionId: item.connectionId,
144
149
  });
150
+ connectionsToUpdate.add(item.connectionId);
145
151
  } catch (err) {
146
152
  errors.push({
147
153
  type: "ingest",
148
154
  id: "unknown",
149
- error: err instanceof Error ? err.message : String(err),
155
+ message: err instanceof Error ? err.message : String(err),
150
156
  });
151
157
  }
152
158
  }
@@ -158,7 +164,7 @@ async function ingestAndUpdate(
158
164
  } as never);
159
165
  }
160
166
 
161
- return { processed, errors, affectedUsers: [...userMap.values()] };
167
+ return { errors, items: result.items };
162
168
  }
163
169
 
164
170
  // ─── Webhook Handlers ────────────────────────────────────────────────────────
@@ -167,8 +173,9 @@ async function ingestAndUpdate(
167
173
  * Handle a webhook for Garmin activities (push or ping mode).
168
174
  */
169
175
  export const handleGarminWebhookActivities = action({
170
- args: { payload: v.any() },
176
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
171
177
  handler: async (ctx, args): Promise<WebhookResult> => {
178
+ const shouldIngest = args.autoIngest !== false;
172
179
  if (isWebhookPushMode(args.payload)) {
173
180
  const pushResult = garminActivityPushPayloadSchema.safeParse(args.payload);
174
181
  if (pushResult.success && pushResult.data.activities.length > 0) {
@@ -176,7 +183,9 @@ export const handleGarminWebhookActivities = action({
176
183
  internal.garmin.private.processActivityPushPayload,
177
184
  { payload: args.payload },
178
185
  );
179
- return await ingestAndUpdate(ctx, result, api.public.ingestActivity);
186
+ return shouldIngest
187
+ ? await ingestAndUpdate(ctx, result, api.public.ingestActivity)
188
+ : toWebhookResult(result);
180
189
  }
181
190
  } else {
182
191
  const pingResult = garminActivityPingPayloadSchema.safeParse(args.payload);
@@ -191,7 +200,7 @@ export const handleGarminWebhookActivities = action({
191
200
  console.warn(
192
201
  `[garmin:webhook:activities] Payload matched neither ping nor push schema`,
193
202
  );
194
- return { processed: 0, errors: [], affectedUsers: [] };
203
+ return { errors: [], items: [] };
195
204
  },
196
205
  });
197
206
 
@@ -199,8 +208,9 @@ export const handleGarminWebhookActivities = action({
199
208
  * Handle a webhook for Garmin activity details (push or ping mode).
200
209
  */
201
210
  export const handleGarminWebhookActivityDetails = action({
202
- args: { payload: v.any() },
211
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
203
212
  handler: async (ctx, args): Promise<WebhookResult> => {
213
+ const shouldIngest = args.autoIngest !== false;
204
214
  if (isWebhookPushMode(args.payload)) {
205
215
  const pushResult =
206
216
  garminActivityDetailsPushPayloadSchema.safeParse(args.payload);
@@ -212,7 +222,9 @@ export const handleGarminWebhookActivityDetails = action({
212
222
  internal.garmin.private.processActivityDetailsPushPayload,
213
223
  { payload: args.payload },
214
224
  );
215
- return await ingestAndUpdate(ctx, result, api.public.ingestActivity);
225
+ return shouldIngest
226
+ ? await ingestAndUpdate(ctx, result, api.public.ingestActivity)
227
+ : toWebhookResult(result);
216
228
  }
217
229
  } else {
218
230
  const pingResult =
@@ -231,7 +243,7 @@ export const handleGarminWebhookActivityDetails = action({
231
243
  console.warn(
232
244
  `[garmin:webhook:activityDetails] Payload matched neither ping nor push schema`,
233
245
  );
234
- return { processed: 0, errors: [], affectedUsers: [] };
246
+ return { errors: [], items: [] };
235
247
  },
236
248
  });
237
249
 
@@ -239,8 +251,9 @@ export const handleGarminWebhookActivityDetails = action({
239
251
  * Handle a webhook for Garmin manually updated activities (push or ping mode).
240
252
  */
241
253
  export const handleGarminWebhookManuallyUpdatedActivities = action({
242
- args: { payload: v.any() },
254
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
243
255
  handler: async (ctx, args): Promise<WebhookResult> => {
256
+ const shouldIngest = args.autoIngest !== false;
244
257
  if (isWebhookPushMode(args.payload)) {
245
258
  const pushResult =
246
259
  garminManuallyUpdatedActivitiesPushPayloadSchema.safeParse(
@@ -255,7 +268,9 @@ export const handleGarminWebhookManuallyUpdatedActivities = action({
255
268
  .processManuallyUpdatedActivitiesPushPayload,
256
269
  { payload: args.payload },
257
270
  );
258
- return await ingestAndUpdate(ctx, result, api.public.ingestActivity);
271
+ return shouldIngest
272
+ ? await ingestAndUpdate(ctx, result, api.public.ingestActivity)
273
+ : toWebhookResult(result);
259
274
  }
260
275
  } else {
261
276
  const pingResult =
@@ -277,7 +292,7 @@ export const handleGarminWebhookManuallyUpdatedActivities = action({
277
292
  console.warn(
278
293
  `[garmin:webhook:manuallyUpdatedActivities] Payload matched neither ping nor push schema`,
279
294
  );
280
- return { processed: 0, errors: [], affectedUsers: [] };
295
+ return { errors: [], items: [] };
281
296
  },
282
297
  });
283
298
 
@@ -285,8 +300,9 @@ export const handleGarminWebhookManuallyUpdatedActivities = action({
285
300
  * Handle a webhook for Garmin Move IQ auto-detected activities (push or ping mode).
286
301
  */
287
302
  export const handleGarminWebhookMoveIQ = action({
288
- args: { payload: v.any() },
303
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
289
304
  handler: async (ctx, args): Promise<WebhookResult> => {
305
+ const shouldIngest = args.autoIngest !== false;
290
306
  if (isWebhookPushMode(args.payload)) {
291
307
  const pushResult =
292
308
  garminMoveIQPushPayloadSchema.safeParse(args.payload);
@@ -298,7 +314,9 @@ export const handleGarminWebhookMoveIQ = action({
298
314
  internal.garmin.private.processMoveIQPushPayload,
299
315
  { payload: args.payload },
300
316
  );
301
- return await ingestAndUpdate(ctx, result, api.public.ingestActivity);
317
+ return shouldIngest
318
+ ? await ingestAndUpdate(ctx, result, api.public.ingestActivity)
319
+ : toWebhookResult(result);
302
320
  }
303
321
  } else {
304
322
  const pingResult =
@@ -317,7 +335,7 @@ export const handleGarminWebhookMoveIQ = action({
317
335
  console.warn(
318
336
  `[garmin:webhook:moveIQ] Payload matched neither ping nor push schema`,
319
337
  );
320
- return { processed: 0, errors: [], affectedUsers: [] };
338
+ return { errors: [], items: [] };
321
339
  },
322
340
  });
323
341
 
@@ -325,8 +343,9 @@ export const handleGarminWebhookMoveIQ = action({
325
343
  * Handle a webhook for Garmin blood pressure summaries (push or ping mode).
326
344
  */
327
345
  export const handleGarminWebhookBloodPressures = action({
328
- args: { payload: v.any() },
346
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
329
347
  handler: async (ctx, args): Promise<WebhookResult> => {
348
+ const shouldIngest = args.autoIngest !== false;
330
349
  if (isWebhookPushMode(args.payload)) {
331
350
  const pushResult =
332
351
  garminBloodPressurePushPayloadSchema.safeParse(args.payload);
@@ -338,7 +357,9 @@ export const handleGarminWebhookBloodPressures = action({
338
357
  internal.garmin.private.processBloodPressurePushPayload,
339
358
  { payload: args.payload },
340
359
  );
341
- return await ingestAndUpdate(ctx, result, api.public.ingestBody);
360
+ return shouldIngest
361
+ ? await ingestAndUpdate(ctx, result, api.public.ingestBody)
362
+ : toWebhookResult(result);
342
363
  }
343
364
  } else {
344
365
  const pingResult =
@@ -357,7 +378,7 @@ export const handleGarminWebhookBloodPressures = action({
357
378
  console.warn(
358
379
  `[garmin:webhook:bloodPressures] Payload matched neither ping nor push schema`,
359
380
  );
360
- return { processed: 0, errors: [], affectedUsers: [] };
381
+ return { errors: [], items: [] };
361
382
  },
362
383
  });
363
384
 
@@ -365,8 +386,9 @@ export const handleGarminWebhookBloodPressures = action({
365
386
  * Handle a webhook for Garmin body composition summaries (push or ping mode).
366
387
  */
367
388
  export const handleGarminWebhookBodyCompositions = action({
368
- args: { payload: v.any() },
389
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
369
390
  handler: async (ctx, args): Promise<WebhookResult> => {
391
+ const shouldIngest = args.autoIngest !== false;
370
392
  if (isWebhookPushMode(args.payload)) {
371
393
  const pushResult =
372
394
  garminBodyCompositionsPushPayloadSchema.safeParse(args.payload);
@@ -378,7 +400,9 @@ export const handleGarminWebhookBodyCompositions = action({
378
400
  internal.garmin.private.processBodyCompositionsPushPayload,
379
401
  { payload: args.payload },
380
402
  );
381
- return await ingestAndUpdate(ctx, result, api.public.ingestBody);
403
+ return shouldIngest
404
+ ? await ingestAndUpdate(ctx, result, api.public.ingestBody)
405
+ : toWebhookResult(result);
382
406
  }
383
407
  } else {
384
408
  const pingResult =
@@ -397,7 +421,7 @@ export const handleGarminWebhookBodyCompositions = action({
397
421
  console.warn(
398
422
  `[garmin:webhook:bodyCompositions] Payload matched neither ping nor push schema`,
399
423
  );
400
- return { processed: 0, errors: [], affectedUsers: [] };
424
+ return { errors: [], items: [] };
401
425
  },
402
426
  });
403
427
 
@@ -405,8 +429,9 @@ export const handleGarminWebhookBodyCompositions = action({
405
429
  * Handle a webhook for Garmin daily summaries (push or ping mode).
406
430
  */
407
431
  export const handleGarminWebhookDailies = action({
408
- args: { payload: v.any() },
432
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
409
433
  handler: async (ctx, args): Promise<WebhookResult> => {
434
+ const shouldIngest = args.autoIngest !== false;
410
435
  if (isWebhookPushMode(args.payload)) {
411
436
  const pushResult =
412
437
  garminDailiesPushPayloadSchema.safeParse(args.payload);
@@ -418,7 +443,9 @@ export const handleGarminWebhookDailies = action({
418
443
  internal.garmin.private.processDailiesPushPayload,
419
444
  { payload: args.payload },
420
445
  );
421
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
446
+ return shouldIngest
447
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
448
+ : toWebhookResult(result);
422
449
  }
423
450
  } else {
424
451
  const pingResult =
@@ -437,7 +464,7 @@ export const handleGarminWebhookDailies = action({
437
464
  console.warn(
438
465
  `[garmin:webhook:dailies] Payload matched neither ping nor push schema`,
439
466
  );
440
- return { processed: 0, errors: [], affectedUsers: [] };
467
+ return { errors: [], items: [] };
441
468
  },
442
469
  });
443
470
 
@@ -445,8 +472,9 @@ export const handleGarminWebhookDailies = action({
445
472
  * Handle a webhook for Garmin epoch summaries (push or ping mode).
446
473
  */
447
474
  export const handleGarminWebhookEpochs = action({
448
- args: { payload: v.any() },
475
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
449
476
  handler: async (ctx, args): Promise<WebhookResult> => {
477
+ const shouldIngest = args.autoIngest !== false;
450
478
  if (isWebhookPushMode(args.payload)) {
451
479
  const pushResult =
452
480
  garminEpochPushPayloadSchema.safeParse(args.payload);
@@ -458,7 +486,9 @@ export const handleGarminWebhookEpochs = action({
458
486
  internal.garmin.private.processEpochPushPayload,
459
487
  { payload: args.payload },
460
488
  );
461
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
489
+ return shouldIngest
490
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
491
+ : toWebhookResult(result);
462
492
  }
463
493
  } else {
464
494
  const pingResult =
@@ -477,7 +507,7 @@ export const handleGarminWebhookEpochs = action({
477
507
  console.warn(
478
508
  `[garmin:webhook:epochs] Payload matched neither ping nor push schema`,
479
509
  );
480
- return { processed: 0, errors: [], affectedUsers: [] };
510
+ return { errors: [], items: [] };
481
511
  },
482
512
  });
483
513
 
@@ -485,8 +515,9 @@ export const handleGarminWebhookEpochs = action({
485
515
  * Handle a webhook for Garmin health snapshot summaries (push or ping mode).
486
516
  */
487
517
  export const handleGarminWebhookHealthSnapshot = action({
488
- args: { payload: v.any() },
518
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
489
519
  handler: async (ctx, args): Promise<WebhookResult> => {
520
+ const shouldIngest = args.autoIngest !== false;
490
521
  if (isWebhookPushMode(args.payload)) {
491
522
  const pushResult =
492
523
  garminHealthSnapshotPushPayloadSchema.safeParse(args.payload);
@@ -498,7 +529,9 @@ export const handleGarminWebhookHealthSnapshot = action({
498
529
  internal.garmin.private.processHealthSnapshotPushPayload,
499
530
  { payload: args.payload },
500
531
  );
501
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
532
+ return shouldIngest
533
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
534
+ : toWebhookResult(result);
502
535
  }
503
536
  } else {
504
537
  const pingResult =
@@ -517,18 +550,17 @@ export const handleGarminWebhookHealthSnapshot = action({
517
550
  console.warn(
518
551
  `[garmin:webhook:healthSnapshot] Payload matched neither ping nor push schema`,
519
552
  );
520
- return { processed: 0, errors: [], affectedUsers: [] };
553
+ return { errors: [], items: [] };
521
554
  },
522
555
  });
523
556
 
524
557
  /**
525
558
  * Handle a webhook for Garmin sleep summaries (push or ping mode).
526
- * Follows the structured dispatch pattern: validates with Zod, then delegates
527
- * to internal actions for processing.
528
559
  */
529
560
  export const handleGarminWebhookSleeps = action({
530
- args: { payload: v.any() },
561
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
531
562
  handler: async (ctx, args): Promise<WebhookResult> => {
563
+ const shouldIngest = args.autoIngest !== false;
532
564
  if (isWebhookPushMode(args.payload)) {
533
565
  const pushResult =
534
566
  garminSleepsPushPayloadSchema.safeParse(args.payload);
@@ -540,7 +572,9 @@ export const handleGarminWebhookSleeps = action({
540
572
  internal.garmin.private.processSleepsPushPayload,
541
573
  { payload: args.payload },
542
574
  );
543
- return await ingestAndUpdate(ctx, result, api.public.ingestSleep);
575
+ return shouldIngest
576
+ ? await ingestAndUpdate(ctx, result, api.public.ingestSleep)
577
+ : toWebhookResult(result);
544
578
  }
545
579
  } else {
546
580
  const pingResult =
@@ -559,18 +593,17 @@ export const handleGarminWebhookSleeps = action({
559
593
  console.warn(
560
594
  `[garmin:webhook:sleeps] Payload matched neither ping nor push schema`,
561
595
  );
562
- return { processed: 0, errors: [], affectedUsers: [] };
596
+ return { errors: [], items: [] };
563
597
  },
564
598
  });
565
599
 
566
600
  /**
567
601
  * Handle a webhook for Garmin skin temperature summaries (push or ping mode).
568
- * Follows the structured dispatch pattern: validates with Zod, then delegates
569
- * to internal actions for processing.
570
602
  */
571
603
  export const handleGarminWebhookSkinTemp = action({
572
- args: { payload: v.any() },
604
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
573
605
  handler: async (ctx, args): Promise<WebhookResult> => {
606
+ const shouldIngest = args.autoIngest !== false;
574
607
  if (isWebhookPushMode(args.payload)) {
575
608
  const pushResult =
576
609
  garminSkinTemperaturePushPayloadSchema.safeParse(args.payload);
@@ -582,7 +615,9 @@ export const handleGarminWebhookSkinTemp = action({
582
615
  internal.garmin.private.processSkinTemperaturePushPayload,
583
616
  { payload: args.payload },
584
617
  );
585
- return await ingestAndUpdate(ctx, result, api.public.ingestBody);
618
+ return shouldIngest
619
+ ? await ingestAndUpdate(ctx, result, api.public.ingestBody)
620
+ : toWebhookResult(result);
586
621
  }
587
622
  } else {
588
623
  const pingResult =
@@ -601,18 +636,17 @@ export const handleGarminWebhookSkinTemp = action({
601
636
  console.warn(
602
637
  `[garmin:webhook:skinTemperature] Payload matched neither ping nor push schema`,
603
638
  );
604
- return { processed: 0, errors: [], affectedUsers: [] };
639
+ return { errors: [], items: [] };
605
640
  },
606
641
  });
607
642
 
608
643
  /**
609
644
  * Handle a webhook for Garmin user metrics (push or ping mode).
610
- * Follows the structured dispatch pattern: validates with Zod, then delegates
611
- * to internal actions for processing.
612
645
  */
613
646
  export const handleGarminWebhookUserMetrics = action({
614
- args: { payload: v.any() },
647
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
615
648
  handler: async (ctx, args): Promise<WebhookResult> => {
649
+ const shouldIngest = args.autoIngest !== false;
616
650
  if (isWebhookPushMode(args.payload)) {
617
651
  const pushResult =
618
652
  garminUserMetricsPushPayloadSchema.safeParse(args.payload);
@@ -624,7 +658,9 @@ export const handleGarminWebhookUserMetrics = action({
624
658
  internal.garmin.private.processUserMetricsPushPayload,
625
659
  { payload: args.payload },
626
660
  );
627
- return await ingestAndUpdate(ctx, result, api.public.ingestBody);
661
+ return shouldIngest
662
+ ? await ingestAndUpdate(ctx, result, api.public.ingestBody)
663
+ : toWebhookResult(result);
628
664
  }
629
665
  } else {
630
666
  const pingResult =
@@ -643,18 +679,17 @@ export const handleGarminWebhookUserMetrics = action({
643
679
  console.warn(
644
680
  `[garmin:webhook:userMetrics] Payload matched neither ping nor push schema`,
645
681
  );
646
- return { processed: 0, errors: [], affectedUsers: [] };
682
+ return { errors: [], items: [] };
647
683
  },
648
684
  });
649
685
 
650
686
  /**
651
687
  * Handle a webhook for Garmin menstrual cycle tracking (push or ping mode).
652
- * Follows the structured dispatch pattern: validates with Zod, then delegates
653
- * to internal actions for processing.
654
688
  */
655
689
  export const handleGarminWebhookMenstrualCycleTracking = action({
656
- args: { payload: v.any() },
690
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
657
691
  handler: async (ctx, args): Promise<WebhookResult> => {
692
+ const shouldIngest = args.autoIngest !== false;
658
693
  if (isWebhookPushMode(args.payload)) {
659
694
  const pushResult =
660
695
  garminMenstrualCycleTrackingPushPayloadSchema.safeParse(args.payload);
@@ -666,7 +701,9 @@ export const handleGarminWebhookMenstrualCycleTracking = action({
666
701
  internal.garmin.private.processMenstrualCycleTrackingPushPayload,
667
702
  { payload: args.payload },
668
703
  );
669
- return await ingestAndUpdate(ctx, result, api.public.ingestMenstruation);
704
+ return shouldIngest
705
+ ? await ingestAndUpdate(ctx, result, api.public.ingestMenstruation)
706
+ : toWebhookResult(result);
670
707
  }
671
708
  } else {
672
709
  const pingResult =
@@ -685,18 +722,17 @@ export const handleGarminWebhookMenstrualCycleTracking = action({
685
722
  console.warn(
686
723
  `[garmin:webhook:menstrualCycleTracking] Payload matched neither ping nor push schema`,
687
724
  );
688
- return { processed: 0, errors: [], affectedUsers: [] };
725
+ return { errors: [], items: [] };
689
726
  },
690
727
  });
691
728
 
692
729
  /**
693
730
  * Handle a webhook for Garmin HRV summaries (push or ping mode).
694
- * Follows the structured dispatch pattern: validates with Zod, then delegates
695
- * to internal actions for processing.
696
731
  */
697
732
  export const handleGarminWebhookHRVSummary = action({
698
- args: { payload: v.any() },
733
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
699
734
  handler: async (ctx, args): Promise<WebhookResult> => {
735
+ const shouldIngest = args.autoIngest !== false;
700
736
  if (isWebhookPushMode(args.payload)) {
701
737
  const pushResult =
702
738
  garminHRVSummaryPushPayloadSchema.safeParse(args.payload);
@@ -708,7 +744,9 @@ export const handleGarminWebhookHRVSummary = action({
708
744
  internal.garmin.private.processHRVSummaryPushPayload,
709
745
  { payload: args.payload },
710
746
  );
711
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
747
+ return shouldIngest
748
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
749
+ : toWebhookResult(result);
712
750
  }
713
751
  } else {
714
752
  const pingResult =
@@ -727,18 +765,17 @@ export const handleGarminWebhookHRVSummary = action({
727
765
  console.warn(
728
766
  `[garmin:webhook:hrvSummary] Payload matched neither ping nor push schema`,
729
767
  );
730
- return { processed: 0, errors: [], affectedUsers: [] };
768
+ return { errors: [], items: [] };
731
769
  },
732
770
  });
733
771
 
734
772
  /**
735
773
  * Handle a webhook for Garmin stress detail summaries (push or ping mode).
736
- * Follows the structured dispatch pattern: validates with Zod, then delegates
737
- * to internal actions for processing.
738
774
  */
739
775
  export const handleGarminWebhookStress = action({
740
- args: { payload: v.any() },
776
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
741
777
  handler: async (ctx, args): Promise<WebhookResult> => {
778
+ const shouldIngest = args.autoIngest !== false;
742
779
  if (isWebhookPushMode(args.payload)) {
743
780
  const pushResult =
744
781
  garminStressPushPayloadSchema.safeParse(args.payload);
@@ -750,7 +787,9 @@ export const handleGarminWebhookStress = action({
750
787
  internal.garmin.private.processStressPushPayload,
751
788
  { payload: args.payload },
752
789
  );
753
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
790
+ return shouldIngest
791
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
792
+ : toWebhookResult(result);
754
793
  }
755
794
  } else {
756
795
  const pingResult =
@@ -769,18 +808,17 @@ export const handleGarminWebhookStress = action({
769
808
  console.warn(
770
809
  `[garmin:webhook:stressDetails] Payload matched neither ping nor push schema`,
771
810
  );
772
- return { processed: 0, errors: [], affectedUsers: [] };
811
+ return { errors: [], items: [] };
773
812
  },
774
813
  });
775
814
 
776
815
  /**
777
816
  * Handle a webhook for Garmin pulse oximetry (SpO2) summaries (push or ping mode).
778
- * Follows the structured dispatch pattern: validates with Zod, then delegates
779
- * to internal actions for processing.
780
817
  */
781
818
  export const handleGarminWebhookPulseOx = action({
782
- args: { payload: v.any() },
819
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
783
820
  handler: async (ctx, args): Promise<WebhookResult> => {
821
+ const shouldIngest = args.autoIngest !== false;
784
822
  if (isWebhookPushMode(args.payload)) {
785
823
  const pushResult =
786
824
  garminPulseOxPushPayloadSchema.safeParse(args.payload);
@@ -792,7 +830,9 @@ export const handleGarminWebhookPulseOx = action({
792
830
  internal.garmin.private.processPulseOxPushPayload,
793
831
  { payload: args.payload },
794
832
  );
795
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
833
+ return shouldIngest
834
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
835
+ : toWebhookResult(result);
796
836
  }
797
837
  } else {
798
838
  const pingResult =
@@ -811,18 +851,17 @@ export const handleGarminWebhookPulseOx = action({
811
851
  console.warn(
812
852
  `[garmin:webhook:pulseOx] Payload matched neither ping nor push schema`,
813
853
  );
814
- return { processed: 0, errors: [], affectedUsers: [] };
854
+ return { errors: [], items: [] };
815
855
  },
816
856
  });
817
857
 
818
858
  /**
819
859
  * Handle a webhook for Garmin respiration summaries (push or ping mode).
820
- * Follows the structured dispatch pattern: validates with Zod, then delegates
821
- * to internal actions for processing.
822
860
  */
823
861
  export const handleGarminWebhookRespiration = action({
824
- args: { payload: v.any() },
862
+ args: { payload: v.any(), autoIngest: v.optional(v.boolean()) },
825
863
  handler: async (ctx, args): Promise<WebhookResult> => {
864
+ const shouldIngest = args.autoIngest !== false;
826
865
  if (isWebhookPushMode(args.payload)) {
827
866
  const pushResult =
828
867
  garminRespirationPushPayloadSchema.safeParse(args.payload);
@@ -834,7 +873,9 @@ export const handleGarminWebhookRespiration = action({
834
873
  internal.garmin.private.processRespirationPushPayload,
835
874
  { payload: args.payload },
836
875
  );
837
- return await ingestAndUpdate(ctx, result, api.public.ingestDaily);
876
+ return shouldIngest
877
+ ? await ingestAndUpdate(ctx, result, api.public.ingestDaily)
878
+ : toWebhookResult(result);
838
879
  }
839
880
  } else {
840
881
  const pingResult =
@@ -853,6 +894,6 @@ export const handleGarminWebhookRespiration = action({
853
894
  console.warn(
854
895
  `[garmin:webhook:respiration] Payload matched neither ping nor push schema`,
855
896
  );
856
- return { processed: 0, errors: [], affectedUsers: [] };
897
+ return { errors: [], items: [] };
857
898
  },
858
899
  });