@nativesquare/soma 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/garmin.d.ts +31 -0
- package/dist/client/garmin.d.ts.map +1 -1
- package/dist/client/garmin.js +34 -0
- package/dist/client/garmin.js.map +1 -1
- package/dist/client/healthkit.d.ts +267 -0
- package/dist/client/healthkit.d.ts.map +1 -0
- package/dist/client/healthkit.js +600 -0
- package/dist/client/healthkit.js.map +1 -0
- package/dist/client/index.d.ts +4 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +3 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +26 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +7 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/private.d.ts +18 -85
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +12 -12
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +38 -65
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +132 -10
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/utils.d.ts +48 -0
- package/dist/component/garmin/utils.d.ts.map +1 -1
- package/dist/component/garmin/utils.js +65 -0
- package/dist/component/garmin/utils.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +1 -11
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/healthkit/index.d.ts +14 -0
- package/dist/component/healthkit/index.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit}/index.js +11 -11
- package/dist/component/healthkit/index.js.map +1 -0
- package/dist/component/healthkit/transform/activity.d.ts +19 -0
- package/dist/component/healthkit/transform/activity.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/activity.js +1 -1
- package/dist/component/healthkit/transform/activity.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/athlete.d.ts +3 -9
- package/dist/component/healthkit/transform/athlete.d.ts.map +1 -0
- package/dist/component/healthkit/transform/athlete.js.map +1 -0
- package/dist/component/healthkit/transform/body.d.ts +25 -0
- package/dist/component/healthkit/transform/body.d.ts.map +1 -0
- package/dist/component/healthkit/transform/body.js.map +1 -0
- package/dist/component/healthkit/transform/daily.d.ts +36 -0
- package/dist/component/healthkit/transform/daily.d.ts.map +1 -0
- package/dist/component/healthkit/transform/daily.js.map +1 -0
- package/dist/{healthkit/maps/activity-type.d.ts → component/healthkit/transform/maps/activityType.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/activityType.d.ts.map +1 -0
- package/dist/{healthkit/maps/activity-type.js → component/healthkit/transform/maps/activityType.js} +1 -1
- package/dist/component/healthkit/transform/maps/activityType.js.map +1 -0
- package/dist/{healthkit/maps/menstruation-flow.d.ts → component/healthkit/transform/maps/menstruationFlow.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/menstruationFlow.d.ts.map +1 -0
- package/dist/{healthkit/maps/menstruation-flow.js → component/healthkit/transform/maps/menstruationFlow.js} +2 -2
- package/dist/component/healthkit/transform/maps/menstruationFlow.js.map +1 -0
- package/dist/{healthkit/maps/sleep-level.d.ts → component/healthkit/transform/maps/sleepLevel.d.ts} +1 -1
- package/dist/component/healthkit/transform/maps/sleepLevel.d.ts.map +1 -0
- package/dist/{healthkit/maps/sleep-level.js → component/healthkit/transform/maps/sleepLevel.js} +2 -2
- package/dist/component/healthkit/transform/maps/sleepLevel.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/menstruation.d.ts +3 -17
- package/dist/component/healthkit/transform/menstruation.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/menstruation.js +1 -1
- package/dist/component/healthkit/transform/menstruation.js.map +1 -0
- package/dist/component/healthkit/transform/nutrition.d.ts +25 -0
- package/dist/component/healthkit/transform/nutrition.d.ts.map +1 -0
- package/dist/component/healthkit/transform/nutrition.js.map +1 -0
- package/dist/component/healthkit/transform/sleep.d.ts +22 -0
- package/dist/component/healthkit/transform/sleep.d.ts.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/sleep.js +2 -2
- package/dist/component/healthkit/transform/sleep.js.map +1 -0
- package/dist/{healthkit → component/healthkit/transform}/utils.d.ts +1 -1
- package/dist/component/healthkit/transform/utils.d.ts.map +1 -0
- package/dist/component/healthkit/transform/utils.js.map +1 -0
- package/dist/component/healthkit/types.d.ts.map +1 -0
- package/dist/component/healthkit/types.js.map +1 -0
- package/dist/component/public.d.ts +3 -3
- package/dist/component/schema.d.ts +4 -4
- package/dist/component/strava/public.d.ts +4 -15
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +4 -3
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/webhooks.d.ts +3 -10
- package/dist/component/strava/webhooks.d.ts.map +1 -1
- package/dist/component/strava/webhooks.js.map +1 -1
- package/dist/component/validators/daily.d.ts +2 -2
- package/dist/component/validators/shared.d.ts +16 -3
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +1 -1
- package/dist/component/validators/shared.js.map +1 -1
- package/dist/validators.d.ts +5 -4
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js.map +1 -1
- package/package.json +3 -3
- package/src/client/garmin.ts +42 -0
- package/src/client/healthkit.ts +791 -0
- package/src/client/index.ts +5 -0
- package/src/client/types.ts +4 -2
- package/src/component/_generated/api.ts +26 -0
- package/src/component/_generated/component.ts +13 -0
- package/src/component/garmin/private.ts +12 -12
- package/src/component/garmin/public.ts +166 -11
- package/src/component/garmin/utils.ts +102 -0
- package/src/component/garmin/webhooks.ts +1 -7
- package/src/{healthkit → component/healthkit}/index.ts +46 -59
- package/src/component/healthkit/transform/activity.ts +115 -0
- package/src/{healthkit → component/healthkit/transform}/athlete.ts +4 -8
- package/src/{healthkit → component/healthkit/transform}/body.ts +3 -7
- package/src/{healthkit → component/healthkit/transform}/daily.ts +4 -10
- package/src/{healthkit/maps/menstruation-flow.ts → component/healthkit/transform/maps/menstruationFlow.ts} +1 -1
- package/src/{healthkit/maps/sleep-level.ts → component/healthkit/transform/maps/sleepLevel.ts} +1 -1
- package/src/{healthkit → component/healthkit/transform}/menstruation.ts +4 -8
- package/src/{healthkit → component/healthkit/transform}/nutrition.ts +3 -7
- package/src/{healthkit → component/healthkit/transform}/sleep.ts +5 -9
- package/src/{healthkit → component/healthkit/transform}/utils.ts +1 -1
- package/src/component/strava/public.ts +6 -5
- package/src/component/strava/webhooks.ts +9 -11
- package/src/component/validators/shared.ts +47 -4
- package/src/validators.ts +1 -0
- package/dist/healthkit/activity.d.ts +0 -75
- package/dist/healthkit/activity.d.ts.map +0 -1
- package/dist/healthkit/activity.js.map +0 -1
- package/dist/healthkit/athlete.d.ts.map +0 -1
- package/dist/healthkit/athlete.js.map +0 -1
- package/dist/healthkit/body.d.ts +0 -102
- package/dist/healthkit/body.d.ts.map +0 -1
- package/dist/healthkit/body.js.map +0 -1
- package/dist/healthkit/daily.d.ts +0 -119
- package/dist/healthkit/daily.d.ts.map +0 -1
- package/dist/healthkit/daily.js.map +0 -1
- package/dist/healthkit/index.d.ts +0 -21
- package/dist/healthkit/index.d.ts.map +0 -1
- package/dist/healthkit/index.js.map +0 -1
- package/dist/healthkit/maps/activity-type.d.ts.map +0 -1
- package/dist/healthkit/maps/activity-type.js.map +0 -1
- package/dist/healthkit/maps/menstruation-flow.d.ts.map +0 -1
- package/dist/healthkit/maps/menstruation-flow.js.map +0 -1
- package/dist/healthkit/maps/sleep-level.d.ts.map +0 -1
- package/dist/healthkit/maps/sleep-level.js.map +0 -1
- package/dist/healthkit/menstruation.d.ts.map +0 -1
- package/dist/healthkit/menstruation.js.map +0 -1
- package/dist/healthkit/nutrition.d.ts +0 -77
- package/dist/healthkit/nutrition.d.ts.map +0 -1
- package/dist/healthkit/nutrition.js.map +0 -1
- package/dist/healthkit/sleep.d.ts +0 -60
- package/dist/healthkit/sleep.d.ts.map +0 -1
- package/dist/healthkit/sleep.js.map +0 -1
- package/dist/healthkit/types.d.ts.map +0 -1
- package/dist/healthkit/types.js.map +0 -1
- package/dist/healthkit/utils.d.ts.map +0 -1
- package/dist/healthkit/utils.js.map +0 -1
- package/src/healthkit/activity.ts +0 -120
- /package/dist/{healthkit → component/healthkit/transform}/athlete.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/body.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/daily.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/nutrition.js +0 -0
- /package/dist/{healthkit → component/healthkit/transform}/utils.js +0 -0
- /package/dist/{healthkit → component/healthkit}/types.d.ts +0 -0
- /package/dist/{healthkit → component/healthkit}/types.js +0 -0
- /package/src/{healthkit/maps/activity-type.ts → component/healthkit/transform/maps/activityType.ts} +0 -0
- /package/src/{healthkit → component/healthkit}/types.ts +0 -0
package/src/client/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
export type {
|
|
13
13
|
ActionCtx,
|
|
14
14
|
SomaError,
|
|
15
|
+
SomaErrorType,
|
|
15
16
|
SomaResult,
|
|
16
17
|
SomaStravaConfig,
|
|
17
18
|
SomaGarminConfig,
|
|
@@ -40,9 +41,11 @@ export type {
|
|
|
40
41
|
} from "./types.js";
|
|
41
42
|
import type { HttpRouter } from "convex/server";
|
|
42
43
|
import { SomaGarmin } from "./garmin.js";
|
|
44
|
+
import { SomaHealthKit } from "./healthkit.js";
|
|
43
45
|
import { SomaStrava } from "./strava.js";
|
|
44
46
|
|
|
45
47
|
export { SomaGarmin } from "./garmin.js";
|
|
48
|
+
export { SomaHealthKit } from "./healthkit.js";
|
|
46
49
|
export { SomaStrava } from "./strava.js";
|
|
47
50
|
export { STRAVA_OAUTH_CALLBACK_PATH, STRAVA_WEBHOOK_BASE_PATH } from "./strava.js";
|
|
48
51
|
export { GARMIN_OAUTH_CALLBACK_PATH, GARMIN_WEBHOOK_BASE_PATH } from "./garmin.js";
|
|
@@ -90,6 +93,7 @@ export class Soma {
|
|
|
90
93
|
private garminConfig?: SomaGarminConfig;
|
|
91
94
|
|
|
92
95
|
public readonly garmin: SomaGarmin;
|
|
96
|
+
public readonly healthkit: SomaHealthKit;
|
|
93
97
|
public readonly strava: SomaStrava;
|
|
94
98
|
|
|
95
99
|
constructor(
|
|
@@ -100,6 +104,7 @@ export class Soma {
|
|
|
100
104
|
this.garminConfig = options?.garmin ?? this.readGarminEnv();
|
|
101
105
|
|
|
102
106
|
this.garmin = new SomaGarmin(this.component, () => this.requireGarminConfig());
|
|
107
|
+
this.healthkit = new SomaHealthKit(this.component);
|
|
103
108
|
this.strava = new SomaStrava(this.component, () => this.requireStravaConfig());
|
|
104
109
|
}
|
|
105
110
|
|
package/src/client/types.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface SomaGarminConfig {
|
|
|
53
53
|
|
|
54
54
|
// ─── Shared Error & Result Types ───────────────────────────────────────────
|
|
55
55
|
|
|
56
|
+
export type { SomaErrorType } from "../component/validators/shared.js";
|
|
57
|
+
|
|
56
58
|
/**
|
|
57
59
|
* A structured error from a Soma operation.
|
|
58
60
|
*
|
|
@@ -61,8 +63,8 @@ export interface SomaGarminConfig {
|
|
|
61
63
|
* (missing connection, nonexistent document) are thrown as exceptions.
|
|
62
64
|
*/
|
|
63
65
|
export interface SomaError {
|
|
64
|
-
/** Category of the failed item
|
|
65
|
-
type:
|
|
66
|
+
/** Category of the failed item — see {@link SomaErrorType} for valid values. */
|
|
67
|
+
type: import("../component/validators/shared.js").SomaErrorType;
|
|
66
68
|
/** Identifier of the failed item, or `"fetch"` for API-level failures. */
|
|
67
69
|
id: string;
|
|
68
70
|
/** Human-readable error description. */
|
|
@@ -55,6 +55,19 @@ import type * as garmin_types_wellnessApi_client_index from "../garmin/types/wel
|
|
|
55
55
|
import type * as garmin_types_wellnessApi_index from "../garmin/types/wellnessApi/index.js";
|
|
56
56
|
import type * as garmin_utils from "../garmin/utils.js";
|
|
57
57
|
import type * as garmin_webhooks from "../garmin/webhooks.js";
|
|
58
|
+
import type * as healthkit_index from "../healthkit/index.js";
|
|
59
|
+
import type * as healthkit_transform_activity from "../healthkit/transform/activity.js";
|
|
60
|
+
import type * as healthkit_transform_athlete from "../healthkit/transform/athlete.js";
|
|
61
|
+
import type * as healthkit_transform_body from "../healthkit/transform/body.js";
|
|
62
|
+
import type * as healthkit_transform_daily from "../healthkit/transform/daily.js";
|
|
63
|
+
import type * as healthkit_transform_maps_activityType from "../healthkit/transform/maps/activityType.js";
|
|
64
|
+
import type * as healthkit_transform_maps_menstruationFlow from "../healthkit/transform/maps/menstruationFlow.js";
|
|
65
|
+
import type * as healthkit_transform_maps_sleepLevel from "../healthkit/transform/maps/sleepLevel.js";
|
|
66
|
+
import type * as healthkit_transform_menstruation from "../healthkit/transform/menstruation.js";
|
|
67
|
+
import type * as healthkit_transform_nutrition from "../healthkit/transform/nutrition.js";
|
|
68
|
+
import type * as healthkit_transform_sleep from "../healthkit/transform/sleep.js";
|
|
69
|
+
import type * as healthkit_transform_utils from "../healthkit/transform/utils.js";
|
|
70
|
+
import type * as healthkit_types from "../healthkit/types.js";
|
|
58
71
|
import type * as private_ from "../private.js";
|
|
59
72
|
import type * as public_ from "../public.js";
|
|
60
73
|
import type * as strava_auth from "../strava/auth.js";
|
|
@@ -137,6 +150,19 @@ const fullApi: ApiFromModules<{
|
|
|
137
150
|
"garmin/types/wellnessApi/index": typeof garmin_types_wellnessApi_index;
|
|
138
151
|
"garmin/utils": typeof garmin_utils;
|
|
139
152
|
"garmin/webhooks": typeof garmin_webhooks;
|
|
153
|
+
"healthkit/index": typeof healthkit_index;
|
|
154
|
+
"healthkit/transform/activity": typeof healthkit_transform_activity;
|
|
155
|
+
"healthkit/transform/athlete": typeof healthkit_transform_athlete;
|
|
156
|
+
"healthkit/transform/body": typeof healthkit_transform_body;
|
|
157
|
+
"healthkit/transform/daily": typeof healthkit_transform_daily;
|
|
158
|
+
"healthkit/transform/maps/activityType": typeof healthkit_transform_maps_activityType;
|
|
159
|
+
"healthkit/transform/maps/menstruationFlow": typeof healthkit_transform_maps_menstruationFlow;
|
|
160
|
+
"healthkit/transform/maps/sleepLevel": typeof healthkit_transform_maps_sleepLevel;
|
|
161
|
+
"healthkit/transform/menstruation": typeof healthkit_transform_menstruation;
|
|
162
|
+
"healthkit/transform/nutrition": typeof healthkit_transform_nutrition;
|
|
163
|
+
"healthkit/transform/sleep": typeof healthkit_transform_sleep;
|
|
164
|
+
"healthkit/transform/utils": typeof healthkit_transform_utils;
|
|
165
|
+
"healthkit/types": typeof healthkit_types;
|
|
140
166
|
private: typeof private_;
|
|
141
167
|
public: typeof public_;
|
|
142
168
|
"strava/auth": typeof strava_auth;
|
|
@@ -25,6 +25,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
25
25
|
{
|
|
26
26
|
garmin: {
|
|
27
27
|
public: {
|
|
28
|
+
backfillAll: FunctionReference<
|
|
29
|
+
"action",
|
|
30
|
+
"internal",
|
|
31
|
+
{
|
|
32
|
+
clientId: string;
|
|
33
|
+
clientSecret: string;
|
|
34
|
+
endTimeInSeconds?: number;
|
|
35
|
+
startTimeInSeconds?: number;
|
|
36
|
+
userId: string;
|
|
37
|
+
},
|
|
38
|
+
any,
|
|
39
|
+
Name
|
|
40
|
+
>;
|
|
28
41
|
completeGarminOAuth: FunctionReference<
|
|
29
42
|
"action",
|
|
30
43
|
"internal",
|
|
@@ -585,7 +585,7 @@ export const processBodyCompositionsPushPayload = internalAction({
|
|
|
585
585
|
if (!connection) {
|
|
586
586
|
for (const item of userItems) {
|
|
587
587
|
errors.push({
|
|
588
|
-
type: "
|
|
588
|
+
type: "body",
|
|
589
589
|
id: item.summaryId ?? "unknown",
|
|
590
590
|
message: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
591
591
|
});
|
|
@@ -596,7 +596,7 @@ export const processBodyCompositionsPushPayload = internalAction({
|
|
|
596
596
|
if (!connection.active) {
|
|
597
597
|
for (const item of userItems) {
|
|
598
598
|
errors.push({
|
|
599
|
-
type: "
|
|
599
|
+
type: "body",
|
|
600
600
|
id: item.summaryId ?? "unknown",
|
|
601
601
|
message: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
602
602
|
});
|
|
@@ -611,7 +611,7 @@ export const processBodyCompositionsPushPayload = internalAction({
|
|
|
611
611
|
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
612
612
|
} catch (err) {
|
|
613
613
|
errors.push({
|
|
614
|
-
type: "
|
|
614
|
+
type: "body",
|
|
615
615
|
id: item.summaryId ?? "unknown",
|
|
616
616
|
message: err instanceof Error ? err.message : String(err),
|
|
617
617
|
});
|
|
@@ -677,7 +677,7 @@ export const processDailiesPushPayload = internalAction({
|
|
|
677
677
|
if (!connection) {
|
|
678
678
|
for (const item of userItems) {
|
|
679
679
|
errors.push({
|
|
680
|
-
type: "
|
|
680
|
+
type: "daily",
|
|
681
681
|
id: item.summaryId ?? "unknown",
|
|
682
682
|
message: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
683
683
|
});
|
|
@@ -688,7 +688,7 @@ export const processDailiesPushPayload = internalAction({
|
|
|
688
688
|
if (!connection.active) {
|
|
689
689
|
for (const item of userItems) {
|
|
690
690
|
errors.push({
|
|
691
|
-
type: "
|
|
691
|
+
type: "daily",
|
|
692
692
|
id: item.summaryId ?? "unknown",
|
|
693
693
|
message: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
694
694
|
});
|
|
@@ -703,7 +703,7 @@ export const processDailiesPushPayload = internalAction({
|
|
|
703
703
|
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
704
704
|
} catch (err) {
|
|
705
705
|
errors.push({
|
|
706
|
-
type: "
|
|
706
|
+
type: "daily",
|
|
707
707
|
id: item.summaryId ?? "unknown",
|
|
708
708
|
message: err instanceof Error ? err.message : String(err),
|
|
709
709
|
});
|
|
@@ -861,7 +861,7 @@ export const processHRVSummaryPushPayload = internalAction({
|
|
|
861
861
|
if (!connection) {
|
|
862
862
|
for (const item of userItems) {
|
|
863
863
|
errors.push({
|
|
864
|
-
type: "
|
|
864
|
+
type: "hrv",
|
|
865
865
|
id: item.summaryId ?? "unknown",
|
|
866
866
|
message: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
867
867
|
});
|
|
@@ -872,7 +872,7 @@ export const processHRVSummaryPushPayload = internalAction({
|
|
|
872
872
|
if (!connection.active) {
|
|
873
873
|
for (const item of userItems) {
|
|
874
874
|
errors.push({
|
|
875
|
-
type: "
|
|
875
|
+
type: "hrv",
|
|
876
876
|
id: item.summaryId ?? "unknown",
|
|
877
877
|
message: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
878
878
|
});
|
|
@@ -887,7 +887,7 @@ export const processHRVSummaryPushPayload = internalAction({
|
|
|
887
887
|
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
888
888
|
} catch (err) {
|
|
889
889
|
errors.push({
|
|
890
|
-
type: "
|
|
890
|
+
type: "hrv",
|
|
891
891
|
id: item.summaryId ?? "unknown",
|
|
892
892
|
message: err instanceof Error ? err.message : String(err),
|
|
893
893
|
});
|
|
@@ -1594,7 +1594,7 @@ export const processMenstrualCycleTrackingPushPayload = internalAction({
|
|
|
1594
1594
|
if (!connection) {
|
|
1595
1595
|
for (const item of userItems) {
|
|
1596
1596
|
errors.push({
|
|
1597
|
-
type: "
|
|
1597
|
+
type: "menstruation",
|
|
1598
1598
|
id: item.summaryId ?? "unknown",
|
|
1599
1599
|
message: `No Soma connection found for Garmin userId "${garminUserId}"`,
|
|
1600
1600
|
});
|
|
@@ -1605,7 +1605,7 @@ export const processMenstrualCycleTrackingPushPayload = internalAction({
|
|
|
1605
1605
|
if (!connection.active) {
|
|
1606
1606
|
for (const item of userItems) {
|
|
1607
1607
|
errors.push({
|
|
1608
|
-
type: "
|
|
1608
|
+
type: "menstruation",
|
|
1609
1609
|
id: item.summaryId ?? "unknown",
|
|
1610
1610
|
message: `Garmin connection for userId "${garminUserId}" is inactive`,
|
|
1611
1611
|
});
|
|
@@ -1619,7 +1619,7 @@ export const processMenstrualCycleTrackingPushPayload = internalAction({
|
|
|
1619
1619
|
items.push({ connectionId: connection._id, userId: connection.userId, data });
|
|
1620
1620
|
} catch (err) {
|
|
1621
1621
|
errors.push({
|
|
1622
|
-
type: "
|
|
1622
|
+
type: "menstruation",
|
|
1623
1623
|
id: item.summaryId ?? "unknown",
|
|
1624
1624
|
message: err instanceof Error ? err.message : String(err),
|
|
1625
1625
|
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
createWellnessClient,
|
|
19
19
|
createTrainingClient,
|
|
20
20
|
} from "./client.js";
|
|
21
|
-
import { buildTimeRangeQuery } from "./utils.js";
|
|
21
|
+
import { buildTimeRangeQuery, buildChunkedTimeRangeQueries, buildBackfillQuery, buildChunkedBackfillQueries } from "./utils.js";
|
|
22
22
|
import {
|
|
23
23
|
createWorkoutV2 as sdkCreateWorkoutV2,
|
|
24
24
|
createWorkoutSchedule as sdkCreateWorkoutSchedule,
|
|
@@ -42,6 +42,19 @@ import {
|
|
|
42
42
|
getStressDetails,
|
|
43
43
|
getPulseox,
|
|
44
44
|
getRespiration,
|
|
45
|
+
getBackfillDailies,
|
|
46
|
+
getBackfillSleeps,
|
|
47
|
+
getBackfillBodycomps,
|
|
48
|
+
getBackfillMct,
|
|
49
|
+
getBackfillBloodPressures,
|
|
50
|
+
getBackfillSkinTemp,
|
|
51
|
+
getBackfillUserMetrics,
|
|
52
|
+
getBackfillHrv,
|
|
53
|
+
getBackfillStressDetails,
|
|
54
|
+
getBackfillPulseox,
|
|
55
|
+
getBackfillRespirationEpoch,
|
|
56
|
+
getBackfillActivities,
|
|
57
|
+
getBackfillActivityDetails,
|
|
45
58
|
} from "./types/wellnessApi/sdk.gen";
|
|
46
59
|
import { transformActivity } from "./transform/activity.js";
|
|
47
60
|
import { transformDailies } from "./transform/dailies.js";
|
|
@@ -57,7 +70,7 @@ import { transformPulseOx } from "./transform/pulseOx.js";
|
|
|
57
70
|
import { transformRespiration } from "./transform/respiration.js";
|
|
58
71
|
import { transformPlannedWorkoutToGarmin } from "./transform/plannedWorkout.js";
|
|
59
72
|
import { api, internal } from "../_generated/api";
|
|
60
|
-
import type { SomaError } from "../validators/shared.js";
|
|
73
|
+
import type { SomaError, SomaErrorType } from "../validators/shared.js";
|
|
61
74
|
|
|
62
75
|
// ─── OAuth ──────────────────────────────────────────────────────────────────
|
|
63
76
|
|
|
@@ -187,8 +200,8 @@ export const disconnectGarmin = action({
|
|
|
187
200
|
try {
|
|
188
201
|
const wellnessClient = createWellnessClient(tokenDoc.accessToken);
|
|
189
202
|
await sdkDereg({ client: wellnessClient });
|
|
190
|
-
} catch {
|
|
191
|
-
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn("[garmin:disconnect] Best-effort deregistration failed:", err instanceof Error ? err.message : err);
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
|
|
@@ -507,11 +520,11 @@ export const pullSkinTemperature = action({
|
|
|
507
520
|
await ctx.runMutation(api.public.ingestBody, { connectionId, userId: args.userId, ...data });
|
|
508
521
|
synced.skinTemp++;
|
|
509
522
|
} catch (err) {
|
|
510
|
-
errors.push({ type: "
|
|
523
|
+
errors.push({ type: "skinTemperature", id: skin.summaryId ?? skin.calendarDate ?? "unknown", message: err instanceof Error ? err.message : String(err) });
|
|
511
524
|
}
|
|
512
525
|
}
|
|
513
526
|
} catch (err) {
|
|
514
|
-
errors.push({ type: "
|
|
527
|
+
errors.push({ type: "skinTemperature", id: "fetch", message: err instanceof Error ? err.message : String(err) });
|
|
515
528
|
}
|
|
516
529
|
|
|
517
530
|
await ctx.runMutation(api.public.updateConnection, { connectionId, lastDataUpdate: new Date().toISOString() });
|
|
@@ -740,14 +753,15 @@ export const pullAll = action({
|
|
|
740
753
|
startTimeInSeconds: args.startTimeInSeconds,
|
|
741
754
|
endTimeInSeconds: args.endTimeInSeconds,
|
|
742
755
|
};
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
{ ref: api.garmin.public.
|
|
756
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
757
|
+
const pullFns: Array<{ ref: any; name: SomaErrorType }> = [
|
|
758
|
+
{ ref: api.garmin.public.pullActivities, name: "activity" },
|
|
759
|
+
{ ref: api.garmin.public.pullDailies, name: "daily" },
|
|
746
760
|
{ ref: api.garmin.public.pullSleep, name: "sleep" },
|
|
747
761
|
{ ref: api.garmin.public.pullBody, name: "body" },
|
|
748
762
|
{ ref: api.garmin.public.pullMenstruation, name: "menstruation" },
|
|
749
|
-
{ ref: api.garmin.public.pullBloodPressures, name: "
|
|
750
|
-
{ ref: api.garmin.public.pullSkinTemperature, name: "
|
|
763
|
+
{ ref: api.garmin.public.pullBloodPressures, name: "bloodPressure" },
|
|
764
|
+
{ ref: api.garmin.public.pullSkinTemperature, name: "skinTemperature" },
|
|
751
765
|
{ ref: api.garmin.public.pullUserMetrics, name: "userMetrics" },
|
|
752
766
|
{ ref: api.garmin.public.pullHRV, name: "hrv" },
|
|
753
767
|
{ ref: api.garmin.public.pullStressDetails, name: "stressDetails" },
|
|
@@ -774,6 +788,147 @@ export const pullAll = action({
|
|
|
774
788
|
});
|
|
775
789
|
|
|
776
790
|
|
|
791
|
+
// ─── Backfill ─────────────────────────────────────────────────────────────
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Request historical data from Garmin's backfill API for all summary types.
|
|
795
|
+
*
|
|
796
|
+
* Unlike pull functions (which query by upload time with a 24h max window),
|
|
797
|
+
* backfill requests data by the time it was *recorded* on the device.
|
|
798
|
+
* Garmin processes backfill asynchronously (HTTP 202) and delivers data
|
|
799
|
+
* via push/ping webhooks — no data is returned synchronously.
|
|
800
|
+
*
|
|
801
|
+
* Health endpoints accept up to 90 days per request.
|
|
802
|
+
* Activity endpoints accept up to 30 days per request (auto-chunked).
|
|
803
|
+
*
|
|
804
|
+
* Defaults to the last 90 days when no time range is provided.
|
|
805
|
+
*/
|
|
806
|
+
export const backfillAll = action({
|
|
807
|
+
args: {
|
|
808
|
+
userId: v.string(),
|
|
809
|
+
clientId: v.string(),
|
|
810
|
+
clientSecret: v.string(),
|
|
811
|
+
startTimeInSeconds: v.optional(v.number()),
|
|
812
|
+
endTimeInSeconds: v.optional(v.number()),
|
|
813
|
+
},
|
|
814
|
+
handler: async (ctx, args) => {
|
|
815
|
+
const { connectionId, accessToken } = await ctx.runAction(
|
|
816
|
+
internal.private.resolveConnectionAndAccessToken,
|
|
817
|
+
{
|
|
818
|
+
userId: args.userId,
|
|
819
|
+
provider: "GARMIN",
|
|
820
|
+
clientId: args.clientId,
|
|
821
|
+
clientSecret: args.clientSecret,
|
|
822
|
+
},
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
const wellnessClient = createWellnessClient(accessToken);
|
|
826
|
+
const timeInput = {
|
|
827
|
+
startTimeInSeconds: args.startTimeInSeconds,
|
|
828
|
+
endTimeInSeconds: args.endTimeInSeconds,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// Health endpoints: up to 90 days, single request each
|
|
832
|
+
const healthQuery = buildBackfillQuery(timeInput);
|
|
833
|
+
|
|
834
|
+
// Activity endpoints: max 30 days per request, chunked
|
|
835
|
+
const activityChunks = buildChunkedBackfillQueries(timeInput);
|
|
836
|
+
|
|
837
|
+
const requested: string[] = [];
|
|
838
|
+
const errors: SomaError[] = [];
|
|
839
|
+
|
|
840
|
+
// ── Health backfills (90-day window) ──
|
|
841
|
+
const healthBackfills: Array<{
|
|
842
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
843
|
+
fn: (opts: any) => Promise<any>;
|
|
844
|
+
name: string;
|
|
845
|
+
}> = [
|
|
846
|
+
{ fn: getBackfillDailies, name: "dailies" },
|
|
847
|
+
{ fn: getBackfillSleeps, name: "sleeps" },
|
|
848
|
+
{ fn: getBackfillBodycomps, name: "bodyComps" },
|
|
849
|
+
{ fn: getBackfillMct, name: "mct" },
|
|
850
|
+
{ fn: getBackfillBloodPressures, name: "bloodPressures" },
|
|
851
|
+
{ fn: getBackfillSkinTemp, name: "skinTemp" },
|
|
852
|
+
{ fn: getBackfillUserMetrics, name: "userMetrics" },
|
|
853
|
+
{ fn: getBackfillHrv, name: "hrv" },
|
|
854
|
+
{ fn: getBackfillStressDetails, name: "stressDetails" },
|
|
855
|
+
{ fn: getBackfillPulseox, name: "pulseOx" },
|
|
856
|
+
{ fn: getBackfillRespirationEpoch, name: "respiration" },
|
|
857
|
+
];
|
|
858
|
+
|
|
859
|
+
for (const { fn, name } of healthBackfills) {
|
|
860
|
+
try {
|
|
861
|
+
const { error } = await fn({
|
|
862
|
+
client: wellnessClient,
|
|
863
|
+
query: healthQuery,
|
|
864
|
+
});
|
|
865
|
+
if (error) {
|
|
866
|
+
errors.push({
|
|
867
|
+
type: name as SomaErrorType,
|
|
868
|
+
id: "backfill",
|
|
869
|
+
message: JSON.stringify(error),
|
|
870
|
+
});
|
|
871
|
+
} else {
|
|
872
|
+
requested.push(name);
|
|
873
|
+
}
|
|
874
|
+
} catch (err) {
|
|
875
|
+
errors.push({
|
|
876
|
+
type: name as SomaErrorType,
|
|
877
|
+
id: "backfill",
|
|
878
|
+
message: err instanceof Error ? err.message : String(err),
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ── Activity backfills (30-day chunked windows) ──
|
|
884
|
+
const activityBackfills: Array<{
|
|
885
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
886
|
+
fn: (opts: any) => Promise<any>;
|
|
887
|
+
name: string;
|
|
888
|
+
}> = [
|
|
889
|
+
{ fn: getBackfillActivities, name: "activities" },
|
|
890
|
+
{ fn: getBackfillActivityDetails, name: "activityDetails" },
|
|
891
|
+
];
|
|
892
|
+
|
|
893
|
+
for (const { fn, name } of activityBackfills) {
|
|
894
|
+
let chunkErrors = 0;
|
|
895
|
+
for (const chunk of activityChunks) {
|
|
896
|
+
try {
|
|
897
|
+
const { error } = await fn({
|
|
898
|
+
client: wellnessClient,
|
|
899
|
+
query: chunk,
|
|
900
|
+
});
|
|
901
|
+
if (error) {
|
|
902
|
+
chunkErrors++;
|
|
903
|
+
errors.push({
|
|
904
|
+
type: name as SomaErrorType,
|
|
905
|
+
id: "backfill",
|
|
906
|
+
message: JSON.stringify(error),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
} catch (err) {
|
|
910
|
+
chunkErrors++;
|
|
911
|
+
errors.push({
|
|
912
|
+
type: name as SomaErrorType,
|
|
913
|
+
id: "backfill",
|
|
914
|
+
message: err instanceof Error ? err.message : String(err),
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (chunkErrors < activityChunks.length) {
|
|
919
|
+
requested.push(name);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
924
|
+
connectionId,
|
|
925
|
+
lastDataUpdate: new Date().toISOString(),
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
return { data: { requested }, errors };
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
|
|
777
932
|
// ─── Push ───────────────────────────────────────────────────────────────────
|
|
778
933
|
|
|
779
934
|
export const pushWorkout = action({
|
|
@@ -39,4 +39,106 @@ export function buildTimeRangeQuery(
|
|
|
39
39
|
uploadEndTimeInSeconds: String(uploadEndTimeInSeconds),
|
|
40
40
|
token: accessToken,
|
|
41
41
|
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build an array of time-range query objects, each spanning at most
|
|
46
|
+
* `maxRangeSeconds`.
|
|
47
|
+
*
|
|
48
|
+
* Garmin's intraday endpoints (respiration, stress, pulse-ox, epochs, HRV)
|
|
49
|
+
* enforce a maximum query window of 86 400 s (24 h). This helper splits the
|
|
50
|
+
* full requested range into consecutive, non-overlapping chunks so the caller
|
|
51
|
+
* can issue one API call per chunk and merge the results.
|
|
52
|
+
*/
|
|
53
|
+
export function buildChunkedTimeRangeQueries(
|
|
54
|
+
input: { startTimeInSeconds?: number; endTimeInSeconds?: number },
|
|
55
|
+
accessToken: string,
|
|
56
|
+
maxRangeSeconds: number,
|
|
57
|
+
nowSeconds = Math.floor(Date.now() / 1000),
|
|
58
|
+
defaultSyncDays = 30,
|
|
59
|
+
) {
|
|
60
|
+
const start =
|
|
61
|
+
input.startTimeInSeconds ?? nowSeconds - defaultSyncDays * 86400;
|
|
62
|
+
const end = input.endTimeInSeconds ?? nowSeconds;
|
|
63
|
+
|
|
64
|
+
const chunks: Array<{
|
|
65
|
+
uploadStartTimeInSeconds: string;
|
|
66
|
+
uploadEndTimeInSeconds: string;
|
|
67
|
+
token: string;
|
|
68
|
+
}> = [];
|
|
69
|
+
|
|
70
|
+
for (let chunkStart = start; chunkStart < end; chunkStart += maxRangeSeconds) {
|
|
71
|
+
const chunkEnd = Math.min(chunkStart + maxRangeSeconds, end);
|
|
72
|
+
chunks.push({
|
|
73
|
+
uploadStartTimeInSeconds: String(chunkStart),
|
|
74
|
+
uploadEndTimeInSeconds: String(chunkEnd),
|
|
75
|
+
token: accessToken,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return chunks;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Backfill Query Helpers ────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const BACKFILL_MAX_HEALTH_DAYS = 90;
|
|
85
|
+
const BACKFILL_MAX_ACTIVITY_DAYS = 30;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a backfill query object for Garmin's `/rest/backfill/*` endpoints.
|
|
89
|
+
*
|
|
90
|
+
* Backfill uses `summaryStartTimeInSeconds` / `summaryEndTimeInSeconds`
|
|
91
|
+
* (based on when data was *recorded*, not uploaded) and does NOT take a
|
|
92
|
+
* `token` query param — auth is via Bearer header only.
|
|
93
|
+
*
|
|
94
|
+
* Health endpoints accept up to 90 days per request.
|
|
95
|
+
* Activity endpoints accept up to 30 days per request.
|
|
96
|
+
*/
|
|
97
|
+
export function buildBackfillQuery(
|
|
98
|
+
input: { startTimeInSeconds?: number; endTimeInSeconds?: number },
|
|
99
|
+
nowSeconds = Math.floor(Date.now() / 1000),
|
|
100
|
+
defaultDays = BACKFILL_MAX_HEALTH_DAYS,
|
|
101
|
+
) {
|
|
102
|
+
const summaryStartTimeInSeconds =
|
|
103
|
+
input.startTimeInSeconds ?? nowSeconds - defaultDays * 86400;
|
|
104
|
+
const summaryEndTimeInSeconds = input.endTimeInSeconds ?? nowSeconds;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
summaryStartTimeInSeconds: String(summaryStartTimeInSeconds),
|
|
108
|
+
summaryEndTimeInSeconds: String(summaryEndTimeInSeconds),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build an array of backfill query objects, each spanning at most
|
|
114
|
+
* `maxDays` days.
|
|
115
|
+
*
|
|
116
|
+
* Activity backfill endpoints enforce a 30-day max per request.
|
|
117
|
+
* This splits a wider range into consecutive, non-overlapping chunks.
|
|
118
|
+
*/
|
|
119
|
+
export function buildChunkedBackfillQueries(
|
|
120
|
+
input: { startTimeInSeconds?: number; endTimeInSeconds?: number },
|
|
121
|
+
maxDays = BACKFILL_MAX_ACTIVITY_DAYS,
|
|
122
|
+
nowSeconds = Math.floor(Date.now() / 1000),
|
|
123
|
+
defaultDays = BACKFILL_MAX_HEALTH_DAYS,
|
|
124
|
+
) {
|
|
125
|
+
const start =
|
|
126
|
+
input.startTimeInSeconds ?? nowSeconds - defaultDays * 86400;
|
|
127
|
+
const end = input.endTimeInSeconds ?? nowSeconds;
|
|
128
|
+
|
|
129
|
+
const maxRangeSeconds = maxDays * 86400;
|
|
130
|
+
const chunks: Array<{
|
|
131
|
+
summaryStartTimeInSeconds: string;
|
|
132
|
+
summaryEndTimeInSeconds: string;
|
|
133
|
+
}> = [];
|
|
134
|
+
|
|
135
|
+
for (let chunkStart = start; chunkStart < end; chunkStart += maxRangeSeconds) {
|
|
136
|
+
const chunkEnd = Math.min(chunkStart + maxRangeSeconds, end);
|
|
137
|
+
chunks.push({
|
|
138
|
+
summaryStartTimeInSeconds: String(chunkStart),
|
|
139
|
+
summaryEndTimeInSeconds: String(chunkEnd),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return chunks;
|
|
42
144
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { v } from "convex/values";
|
|
7
7
|
import { action, type ActionCtx } from "../_generated/server";
|
|
8
8
|
import { api, internal } from "../_generated/api";
|
|
9
|
-
import type { SomaError } from "../validators/shared.js";
|
|
9
|
+
import type { SomaError, WebhookResult } from "../validators/shared.js";
|
|
10
10
|
import {
|
|
11
11
|
garminSkinTemperaturePingPayloadSchema,
|
|
12
12
|
garminSkinTemperaturePushPayloadSchema,
|
|
@@ -104,12 +104,6 @@ function isWebhookPushMode(payload: unknown): boolean {
|
|
|
104
104
|
return !("callbackURL" in firstItem);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** Shape returned by webhook handler actions (both internal processors and public handlers). */
|
|
108
|
-
type WebhookResult = {
|
|
109
|
-
errors: SomaError[];
|
|
110
|
-
items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> }>;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
107
|
/**
|
|
114
108
|
* Ingest transformed items from a private handler and update connections.
|
|
115
109
|
* Shared orchestration logic for all push-mode webhook handlers.
|