@nativesquare/soma 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +83 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +131 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +159 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +190 -6
- package/dist/component/garmin.d.ts.map +1 -1
- package/dist/component/garmin.js +805 -25
- package/dist/component/garmin.js.map +1 -1
- package/dist/component/private.d.ts +18 -0
- package/dist/component/private.d.ts.map +1 -1
- package/dist/component/private.js +18 -0
- package/dist/component/private.js.map +1 -1
- package/dist/component/public.d.ts +88 -42
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +12 -2
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +87 -32
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +2 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators/connection.d.ts +1 -0
- package/dist/component/validators/connection.d.ts.map +1 -1
- package/dist/component/validators/connection.js +2 -0
- package/dist/component/validators/connection.js.map +1 -1
- package/dist/component/validators/daily.d.ts +40 -5
- package/dist/component/validators/daily.d.ts.map +1 -1
- package/dist/component/validators/daily.js +10 -1
- package/dist/component/validators/daily.js.map +1 -1
- package/dist/component/validators/enums.d.ts +1 -1
- package/dist/component/validators/plannedWorkout.d.ts +5 -1
- package/dist/component/validators/plannedWorkout.d.ts.map +1 -1
- package/dist/component/validators/plannedWorkout.js +4 -0
- package/dist/component/validators/plannedWorkout.js.map +1 -1
- package/dist/component/validators/sleep.d.ts +8 -8
- package/dist/garmin/activity.d.ts +7 -16
- package/dist/garmin/activity.d.ts.map +1 -1
- package/dist/garmin/activity.js +17 -23
- package/dist/garmin/activity.js.map +1 -1
- package/dist/garmin/bloodPressure.d.ts +28 -0
- package/dist/garmin/bloodPressure.d.ts.map +1 -0
- package/dist/garmin/bloodPressure.js +34 -0
- package/dist/garmin/bloodPressure.js.map +1 -0
- package/dist/garmin/body.js +1 -1
- package/dist/garmin/body.js.map +1 -1
- package/dist/garmin/client.d.ts +117 -17
- package/dist/garmin/client.d.ts.map +1 -1
- package/dist/garmin/client.js +337 -43
- package/dist/garmin/client.js.map +1 -1
- package/dist/garmin/daily.d.ts.map +1 -1
- package/dist/garmin/daily.js +3 -3
- package/dist/garmin/daily.js.map +1 -1
- package/dist/garmin/hrv.d.ts +30 -0
- package/dist/garmin/hrv.d.ts.map +1 -0
- package/dist/garmin/hrv.js +45 -0
- package/dist/garmin/hrv.js.map +1 -0
- package/dist/garmin/index.d.ts +16 -2
- package/dist/garmin/index.d.ts.map +1 -1
- package/dist/garmin/index.js +8 -1
- package/dist/garmin/index.js.map +1 -1
- package/dist/garmin/maps/activity-type.d.ts +1 -2
- package/dist/garmin/maps/activity-type.d.ts.map +1 -1
- package/dist/garmin/maps/activity-type.js +1 -0
- package/dist/garmin/maps/activity-type.js.map +1 -1
- package/dist/garmin/menstruation.d.ts +6 -4
- package/dist/garmin/menstruation.d.ts.map +1 -1
- package/dist/garmin/menstruation.js +12 -8
- package/dist/garmin/menstruation.js.map +1 -1
- package/dist/garmin/pulseOx.d.ts +24 -0
- package/dist/garmin/pulseOx.d.ts.map +1 -0
- package/dist/garmin/pulseOx.js +33 -0
- package/dist/garmin/pulseOx.js.map +1 -0
- package/dist/garmin/respiration.d.ts +29 -0
- package/dist/garmin/respiration.d.ts.map +1 -0
- package/dist/garmin/respiration.js +42 -0
- package/dist/garmin/respiration.js.map +1 -0
- package/dist/garmin/skinTemp.d.ts +27 -0
- package/dist/garmin/skinTemp.d.ts.map +1 -0
- package/dist/garmin/skinTemp.js +35 -0
- package/dist/garmin/skinTemp.js.map +1 -0
- package/dist/garmin/sleep.d.ts +4 -4
- package/dist/garmin/sleep.d.ts.map +1 -1
- package/dist/garmin/sleep.js +15 -9
- package/dist/garmin/sleep.js.map +1 -1
- package/dist/garmin/stressDetails.d.ts +30 -0
- package/dist/garmin/stressDetails.d.ts.map +1 -0
- package/dist/garmin/stressDetails.js +49 -0
- package/dist/garmin/stressDetails.js.map +1 -0
- package/dist/garmin/sync.d.ts +14 -0
- package/dist/garmin/sync.d.ts.map +1 -1
- package/dist/garmin/sync.js +287 -5
- package/dist/garmin/sync.js.map +1 -1
- package/dist/garmin/types.d.ts +77 -186
- package/dist/garmin/types.d.ts.map +1 -1
- package/dist/garmin/types.js +4 -2
- package/dist/garmin/types.js.map +1 -1
- package/dist/garmin/userMetrics.d.ts +23 -0
- package/dist/garmin/userMetrics.d.ts.map +1 -0
- package/dist/garmin/userMetrics.js +41 -0
- package/dist/garmin/userMetrics.js.map +1 -0
- package/dist/validators.d.ts +107 -28
- package/dist/validators.d.ts.map +1 -1
- package/package.json +133 -124
- package/src/client/index.ts +199 -0
- package/src/component/_generated/component.ts +161 -2
- package/src/component/garmin.ts +898 -26
- package/src/component/private.ts +21 -0
- package/src/component/public.ts +11 -2
- package/src/component/schema.ts +2 -1
- package/src/component/validators/connection.ts +2 -0
- package/src/component/validators/daily.ts +15 -0
- package/src/component/validators/plannedWorkout.ts +4 -0
- package/src/garmin/activity.test.ts +13 -21
- package/src/garmin/activity.ts +38 -45
- package/src/garmin/bloodPressure.ts +41 -0
- package/src/garmin/body.ts +1 -1
- package/src/garmin/client.ts +550 -71
- package/src/garmin/daily.ts +8 -4
- package/src/garmin/hrv.ts +57 -0
- package/src/garmin/index.ts +77 -7
- package/src/garmin/maps/activity-type.ts +2 -2
- package/src/garmin/menstruation.ts +14 -12
- package/src/garmin/pulseOx.ts +45 -0
- package/src/garmin/respiration.ts +55 -0
- package/src/garmin/skinTemp.ts +42 -0
- package/src/garmin/sleep.test.ts +5 -6
- package/src/garmin/sleep.ts +22 -16
- package/src/garmin/spec/wellness-api.json +1 -0
- package/src/garmin/stressDetails.ts +71 -0
- package/src/garmin/sync.ts +348 -5
- package/src/garmin/types.ts +88 -300
- package/src/garmin/userMetrics.ts +50 -0
- package/src/garmin/wellness-api.d.ts +5637 -0
package/src/garmin/client.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
// ─── Garmin Health API Client ────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Uses openapi-fetch for type-safe Wellness API calls and manual fetch for
|
|
3
|
+
// Training API endpoints that are not covered by the OpenAPI spec.
|
|
4
4
|
|
|
5
|
+
import createClient from "openapi-fetch";
|
|
6
|
+
import type { Middleware } from "openapi-fetch";
|
|
7
|
+
import type { paths } from "./wellness-api.js";
|
|
5
8
|
import type {
|
|
6
9
|
GarminActivity,
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
GarminActivityDetail,
|
|
11
|
+
GarminDailyExtended,
|
|
12
|
+
GarminSleepExtended,
|
|
9
13
|
GarminBodyComposition,
|
|
10
|
-
|
|
14
|
+
GarminMenstrualCycle,
|
|
15
|
+
GarminUserMetrics,
|
|
16
|
+
GarminStressDetail,
|
|
17
|
+
GarminSkinTemperature,
|
|
18
|
+
GarminRespiration,
|
|
19
|
+
GarminPulseOx,
|
|
20
|
+
GarminMoveIQEvent,
|
|
21
|
+
GarminHRVSummary,
|
|
22
|
+
GarminHealthSnapshot,
|
|
23
|
+
GarminEpoch,
|
|
24
|
+
GarminBloodPressure,
|
|
25
|
+
GarminSolar,
|
|
11
26
|
GarminWorkout,
|
|
12
27
|
GarminWorkoutSchedule,
|
|
13
28
|
} from "./types.js";
|
|
@@ -25,7 +40,10 @@ export interface GarminClientOptions {
|
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
/**
|
|
28
|
-
* A
|
|
43
|
+
* A client for the Garmin Health API.
|
|
44
|
+
*
|
|
45
|
+
* Wellness API endpoints use openapi-fetch for type safety. Training API
|
|
46
|
+
* endpoints use manual fetch since they are not part of the Wellness API spec.
|
|
29
47
|
*
|
|
30
48
|
* All requests are authenticated with a Bearer token. Time-range parameters
|
|
31
49
|
* use Unix epoch seconds for `uploadStartTimeInSeconds` and
|
|
@@ -46,27 +64,89 @@ export interface GarminClientOptions {
|
|
|
46
64
|
export class GarminClient {
|
|
47
65
|
private readonly accessToken: string;
|
|
48
66
|
private readonly baseUrl: string;
|
|
67
|
+
private readonly wellness;
|
|
49
68
|
|
|
50
69
|
constructor(opts: GarminClientOptions) {
|
|
51
70
|
this.accessToken = opts.accessToken;
|
|
52
71
|
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
72
|
+
|
|
73
|
+
const authMiddleware: Middleware = {
|
|
74
|
+
async onRequest({ request }) {
|
|
75
|
+
request.headers.set("Authorization", `Bearer ${opts.accessToken}`);
|
|
76
|
+
request.headers.set("Accept", "application/json");
|
|
77
|
+
return request;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.wellness = createClient<paths>({
|
|
82
|
+
baseUrl: `${this.baseUrl}/wellness-api`,
|
|
83
|
+
});
|
|
84
|
+
this.wellness.use(authMiddleware);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── User Identity ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the Garmin user ID for the authenticated user.
|
|
91
|
+
*
|
|
92
|
+
* Garmin API: `GET /wellness-api/rest/user/id`
|
|
93
|
+
*/
|
|
94
|
+
async getUserId(): Promise<string | null> {
|
|
95
|
+
const { data, error, response } = await this.wellness.GET("/rest/user/id");
|
|
96
|
+
if (error) {
|
|
97
|
+
throw new GarminApiError(
|
|
98
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
99
|
+
response.status,
|
|
100
|
+
JSON.stringify(error),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return data?.userId ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── User Permissions ─────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check which permissions the user has granted.
|
|
110
|
+
*
|
|
111
|
+
* Garmin API: `GET /wellness-api/rest/user/permissions`
|
|
112
|
+
*/
|
|
113
|
+
async getUserPermissions(): Promise<string[]> {
|
|
114
|
+
const { data, error, response } = await this.wellness.GET(
|
|
115
|
+
"/rest/user/permissions",
|
|
116
|
+
);
|
|
117
|
+
if (error) {
|
|
118
|
+
throw new GarminApiError(
|
|
119
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
120
|
+
response.status,
|
|
121
|
+
JSON.stringify(error),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return data?.permissions ?? [];
|
|
53
125
|
}
|
|
54
126
|
|
|
55
|
-
// ─── Daily Summaries
|
|
127
|
+
// ─── Daily Summaries ──────────────────────────────────────────────────
|
|
56
128
|
|
|
57
129
|
/**
|
|
58
130
|
* Get daily wellness summaries.
|
|
59
131
|
*
|
|
60
132
|
* Garmin API: `GET /wellness-api/rest/dailies`
|
|
61
133
|
*/
|
|
62
|
-
async getDailies(params: TimeRangeParams): Promise<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
134
|
+
async getDailies(params: TimeRangeParams): Promise<GarminDailyExtended[]> {
|
|
135
|
+
const { data, error, response } = await this.wellness.GET("/rest/dailies", {
|
|
136
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
137
|
+
});
|
|
138
|
+
if (error) {
|
|
139
|
+
throw new GarminApiError(
|
|
140
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
141
|
+
response.status,
|
|
142
|
+
JSON.stringify(error),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
console.log("[getDailies] raw response:", JSON.stringify(data?.[0], null, 2));
|
|
146
|
+
return (data ?? []) as GarminDailyExtended[];
|
|
67
147
|
}
|
|
68
148
|
|
|
69
|
-
// ─── Activities
|
|
149
|
+
// ─── Activities ───────────────────────────────────────────────────────
|
|
70
150
|
|
|
71
151
|
/**
|
|
72
152
|
* Get activity summaries.
|
|
@@ -74,24 +154,97 @@ export class GarminClient {
|
|
|
74
154
|
* Garmin API: `GET /wellness-api/rest/activities`
|
|
75
155
|
*/
|
|
76
156
|
async getActivities(params: TimeRangeParams): Promise<GarminActivity[]> {
|
|
77
|
-
|
|
78
|
-
"/
|
|
79
|
-
|
|
157
|
+
const { data, error, response } = await this.wellness.GET(
|
|
158
|
+
"/rest/activities",
|
|
159
|
+
{
|
|
160
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
if (error) {
|
|
164
|
+
throw new GarminApiError(
|
|
165
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
166
|
+
response.status,
|
|
167
|
+
JSON.stringify(error),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return (data ?? []) as GarminActivity[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Activity Details ─────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get detailed activity summaries including GPS, heart rate, and sensor data.
|
|
177
|
+
*
|
|
178
|
+
* Garmin API: `GET /wellness-api/rest/activityDetails`
|
|
179
|
+
*/
|
|
180
|
+
async getActivityDetails(
|
|
181
|
+
params: TimeRangeParams,
|
|
182
|
+
): Promise<GarminActivityDetail[]> {
|
|
183
|
+
const { data, error, response } = await this.wellness.GET(
|
|
184
|
+
"/rest/activityDetails",
|
|
185
|
+
{
|
|
186
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
187
|
+
},
|
|
80
188
|
);
|
|
189
|
+
if (error) {
|
|
190
|
+
throw new GarminApiError(
|
|
191
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
192
|
+
response.status,
|
|
193
|
+
JSON.stringify(error),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return (data ?? []) as GarminActivityDetail[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Activity File ────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Download a raw activity file (FIT, TCX, or GPX).
|
|
203
|
+
*
|
|
204
|
+
* Garmin API: `GET /wellness-api/rest/activityFile`
|
|
205
|
+
*
|
|
206
|
+
* @returns The raw file as an ArrayBuffer.
|
|
207
|
+
*/
|
|
208
|
+
async getActivityFile(id: string): Promise<ArrayBuffer> {
|
|
209
|
+
const url = `${this.baseUrl}/wellness-api/rest/activityFile?id=${encodeURIComponent(id)}`;
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
method: "GET",
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const body = await response.text().catch(() => "");
|
|
219
|
+
throw new GarminApiError(
|
|
220
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
221
|
+
response.status,
|
|
222
|
+
body,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return response.arrayBuffer();
|
|
81
227
|
}
|
|
82
228
|
|
|
83
|
-
// ─── Sleep
|
|
229
|
+
// ─── Sleep ────────────────────────────────────────────────────────────
|
|
84
230
|
|
|
85
231
|
/**
|
|
86
232
|
* Get sleep summaries.
|
|
87
233
|
*
|
|
88
234
|
* Garmin API: `GET /wellness-api/rest/sleeps`
|
|
89
235
|
*/
|
|
90
|
-
async getSleeps(params: TimeRangeParams): Promise<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
236
|
+
async getSleeps(params: TimeRangeParams): Promise<GarminSleepExtended[]> {
|
|
237
|
+
const { data, error, response } = await this.wellness.GET("/rest/sleeps", {
|
|
238
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
239
|
+
});
|
|
240
|
+
if (error) {
|
|
241
|
+
throw new GarminApiError(
|
|
242
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
243
|
+
response.status,
|
|
244
|
+
JSON.stringify(error),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return (data ?? []) as GarminSleepExtended[];
|
|
95
248
|
}
|
|
96
249
|
|
|
97
250
|
// ─── Body Composition ─────────────────────────────────────────────────
|
|
@@ -104,26 +257,341 @@ export class GarminClient {
|
|
|
104
257
|
async getBodyCompositions(
|
|
105
258
|
params: TimeRangeParams,
|
|
106
259
|
): Promise<GarminBodyComposition[]> {
|
|
107
|
-
|
|
108
|
-
"/
|
|
109
|
-
|
|
260
|
+
const { data, error, response } = await this.wellness.GET(
|
|
261
|
+
"/rest/bodyComps",
|
|
262
|
+
{
|
|
263
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
264
|
+
},
|
|
110
265
|
);
|
|
266
|
+
if (error) {
|
|
267
|
+
throw new GarminApiError(
|
|
268
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
269
|
+
response.status,
|
|
270
|
+
JSON.stringify(error),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return (data ?? []) as GarminBodyComposition[];
|
|
111
274
|
}
|
|
112
275
|
|
|
113
276
|
// ─── Menstrual Cycle ──────────────────────────────────────────────────
|
|
114
277
|
|
|
115
278
|
/**
|
|
116
|
-
* Get menstrual cycle data.
|
|
279
|
+
* Get menstrual cycle tracking data.
|
|
117
280
|
*
|
|
118
|
-
* Garmin API: `GET /wellness-api/rest/
|
|
281
|
+
* Garmin API: `GET /wellness-api/rest/mct`
|
|
119
282
|
*/
|
|
120
283
|
async getMenstrualCycleData(
|
|
121
284
|
params: TimeRangeParams,
|
|
122
|
-
): Promise<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
285
|
+
): Promise<GarminMenstrualCycle[]> {
|
|
286
|
+
const { data, error, response } = await this.wellness.GET("/rest/mct", {
|
|
287
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
288
|
+
});
|
|
289
|
+
if (error) {
|
|
290
|
+
throw new GarminApiError(
|
|
291
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
292
|
+
response.status,
|
|
293
|
+
JSON.stringify(error),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return (data ?? []) as GarminMenstrualCycle[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── User Metrics ─────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get user metrics (VO2 max, fitness age, etc.).
|
|
303
|
+
*
|
|
304
|
+
* Garmin API: `GET /wellness-api/rest/userMetrics`
|
|
305
|
+
*/
|
|
306
|
+
async getUserMetrics(
|
|
307
|
+
params: TimeRangeParams,
|
|
308
|
+
): Promise<GarminUserMetrics[]> {
|
|
309
|
+
const { data, error, response } = await this.wellness.GET(
|
|
310
|
+
"/rest/userMetrics",
|
|
311
|
+
{
|
|
312
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
if (error) {
|
|
316
|
+
throw new GarminApiError(
|
|
317
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
318
|
+
response.status,
|
|
319
|
+
JSON.stringify(error),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return (data ?? []) as GarminUserMetrics[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Stress Details ───────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get stress detail summaries.
|
|
329
|
+
*
|
|
330
|
+
* Garmin API: `GET /wellness-api/rest/stressDetails`
|
|
331
|
+
*/
|
|
332
|
+
async getStressDetails(
|
|
333
|
+
params: TimeRangeParams,
|
|
334
|
+
): Promise<GarminStressDetail[]> {
|
|
335
|
+
const { data, error, response } = await this.wellness.GET(
|
|
336
|
+
"/rest/stressDetails",
|
|
337
|
+
{
|
|
338
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
if (error) {
|
|
342
|
+
throw new GarminApiError(
|
|
343
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
344
|
+
response.status,
|
|
345
|
+
JSON.stringify(error),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return (data ?? []) as GarminStressDetail[];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─── Skin Temperature ─────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get skin temperature summaries.
|
|
355
|
+
*
|
|
356
|
+
* Garmin API: `GET /wellness-api/rest/skinTemp`
|
|
357
|
+
*/
|
|
358
|
+
async getSkinTemperature(
|
|
359
|
+
params: TimeRangeParams,
|
|
360
|
+
): Promise<GarminSkinTemperature[]> {
|
|
361
|
+
const { data, error, response } = await this.wellness.GET(
|
|
362
|
+
"/rest/skinTemp",
|
|
363
|
+
{
|
|
364
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
if (error) {
|
|
368
|
+
throw new GarminApiError(
|
|
369
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
370
|
+
response.status,
|
|
371
|
+
JSON.stringify(error),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return (data ?? []) as GarminSkinTemperature[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Respiration ──────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get respiration summaries.
|
|
381
|
+
*
|
|
382
|
+
* Garmin API: `GET /wellness-api/rest/respiration`
|
|
383
|
+
*/
|
|
384
|
+
async getRespiration(
|
|
385
|
+
params: TimeRangeParams,
|
|
386
|
+
): Promise<GarminRespiration[]> {
|
|
387
|
+
const { data, error, response } = await this.wellness.GET(
|
|
388
|
+
"/rest/respiration",
|
|
389
|
+
{
|
|
390
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
if (error) {
|
|
394
|
+
throw new GarminApiError(
|
|
395
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
396
|
+
response.status,
|
|
397
|
+
JSON.stringify(error),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return (data ?? []) as GarminRespiration[];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── Pulse Ox ─────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get pulse oximetry (SpO2) summaries.
|
|
407
|
+
*
|
|
408
|
+
* Garmin API: `GET /wellness-api/rest/pulseOx`
|
|
409
|
+
*/
|
|
410
|
+
async getPulseOx(params: TimeRangeParams): Promise<GarminPulseOx[]> {
|
|
411
|
+
const { data, error, response } = await this.wellness.GET(
|
|
412
|
+
"/rest/pulseOx",
|
|
413
|
+
{
|
|
414
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
if (error) {
|
|
418
|
+
throw new GarminApiError(
|
|
419
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
420
|
+
response.status,
|
|
421
|
+
JSON.stringify(error),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return (data ?? []) as GarminPulseOx[];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Move IQ ──────────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get Move IQ auto-detected activity events.
|
|
431
|
+
*
|
|
432
|
+
* Garmin API: `GET /wellness-api/rest/moveiq`
|
|
433
|
+
*/
|
|
434
|
+
async getMoveIQ(params: TimeRangeParams): Promise<GarminMoveIQEvent[]> {
|
|
435
|
+
const { data, error, response } = await this.wellness.GET(
|
|
436
|
+
"/rest/moveiq",
|
|
437
|
+
{
|
|
438
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
if (error) {
|
|
442
|
+
throw new GarminApiError(
|
|
443
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
444
|
+
response.status,
|
|
445
|
+
JSON.stringify(error),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return (data ?? []) as GarminMoveIQEvent[];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── HRV ──────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get heart rate variability (HRV) summaries.
|
|
455
|
+
*
|
|
456
|
+
* Garmin API: `GET /wellness-api/rest/hrv`
|
|
457
|
+
*/
|
|
458
|
+
async getHRV(params: TimeRangeParams): Promise<GarminHRVSummary[]> {
|
|
459
|
+
const { data, error, response } = await this.wellness.GET("/rest/hrv", {
|
|
460
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
461
|
+
});
|
|
462
|
+
if (error) {
|
|
463
|
+
throw new GarminApiError(
|
|
464
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
465
|
+
response.status,
|
|
466
|
+
JSON.stringify(error),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return (data ?? []) as GarminHRVSummary[];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ─── Health Snapshot ──────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get health snapshot summaries.
|
|
476
|
+
*
|
|
477
|
+
* Garmin API: `GET /wellness-api/rest/healthSnapshot`
|
|
478
|
+
*/
|
|
479
|
+
async getHealthSnapshot(
|
|
480
|
+
params: TimeRangeParams,
|
|
481
|
+
): Promise<GarminHealthSnapshot[]> {
|
|
482
|
+
const { data, error, response } = await this.wellness.GET(
|
|
483
|
+
"/rest/healthSnapshot",
|
|
484
|
+
{
|
|
485
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
if (error) {
|
|
489
|
+
throw new GarminApiError(
|
|
490
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
491
|
+
response.status,
|
|
492
|
+
JSON.stringify(error),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return (data ?? []) as GarminHealthSnapshot[];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── Epochs ───────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get epoch (15-minute interval) summaries.
|
|
502
|
+
*
|
|
503
|
+
* Garmin API: `GET /wellness-api/rest/epochs`
|
|
504
|
+
*/
|
|
505
|
+
async getEpochs(params: TimeRangeParams): Promise<GarminEpoch[]> {
|
|
506
|
+
const { data, error, response } = await this.wellness.GET("/rest/epochs", {
|
|
507
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
508
|
+
});
|
|
509
|
+
if (error) {
|
|
510
|
+
throw new GarminApiError(
|
|
511
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
512
|
+
response.status,
|
|
513
|
+
JSON.stringify(error),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return (data ?? []) as GarminEpoch[];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ─── Blood Pressure ───────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get blood pressure summaries.
|
|
523
|
+
*
|
|
524
|
+
* Garmin API: `GET /wellness-api/rest/bloodPressures`
|
|
525
|
+
*/
|
|
526
|
+
async getBloodPressures(
|
|
527
|
+
params: TimeRangeParams,
|
|
528
|
+
): Promise<GarminBloodPressure[]> {
|
|
529
|
+
const { data, error, response } = await this.wellness.GET(
|
|
530
|
+
"/rest/bloodPressures",
|
|
531
|
+
{
|
|
532
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
if (error) {
|
|
536
|
+
throw new GarminApiError(
|
|
537
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
538
|
+
response.status,
|
|
539
|
+
JSON.stringify(error),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return (data ?? []) as GarminBloodPressure[];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ─── Manually Updated Activities ──────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Get manually created or edited activities.
|
|
549
|
+
*
|
|
550
|
+
* Garmin API: `GET /wellness-api/rest/manuallyUpdatedActivities`
|
|
551
|
+
*/
|
|
552
|
+
async getManuallyUpdatedActivities(
|
|
553
|
+
params: TimeRangeParams,
|
|
554
|
+
): Promise<GarminActivity[]> {
|
|
555
|
+
const { data, error, response } = await this.wellness.GET(
|
|
556
|
+
"/rest/manuallyUpdatedActivities",
|
|
557
|
+
{
|
|
558
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
if (error) {
|
|
562
|
+
throw new GarminApiError(
|
|
563
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
564
|
+
response.status,
|
|
565
|
+
JSON.stringify(error),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return (data ?? []) as GarminActivity[];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Solar Intensity ──────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get solar intensity summaries.
|
|
575
|
+
*
|
|
576
|
+
* Garmin API: `GET /wellness-api/rest/solarIntensity`
|
|
577
|
+
*/
|
|
578
|
+
async getSolarIntensity(
|
|
579
|
+
params: TimeRangeParams,
|
|
580
|
+
): Promise<GarminSolar[]> {
|
|
581
|
+
const { data, error, response } = await this.wellness.GET(
|
|
582
|
+
"/rest/solarIntensity",
|
|
583
|
+
{
|
|
584
|
+
params: { query: timeRangeQuery(params, this.accessToken) },
|
|
585
|
+
},
|
|
126
586
|
);
|
|
587
|
+
if (error) {
|
|
588
|
+
throw new GarminApiError(
|
|
589
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
590
|
+
response.status,
|
|
591
|
+
JSON.stringify(error),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return (data ?? []) as GarminSolar[];
|
|
127
595
|
}
|
|
128
596
|
|
|
129
597
|
// ─── Backfill ─────────────────────────────────────────────────────────
|
|
@@ -134,6 +602,10 @@ export class GarminClient {
|
|
|
134
602
|
* Garmin processes backfill requests asynchronously and delivers data
|
|
135
603
|
* via the configured webhook endpoint. Maximum range: 90 days per request.
|
|
136
604
|
*
|
|
605
|
+
* Uses manual fetch because the spec defines individual backfill paths
|
|
606
|
+
* (e.g. `/rest/backfill/dailies`) but this method accepts a dynamic
|
|
607
|
+
* summaryType string.
|
|
608
|
+
*
|
|
137
609
|
* @param summaryType - The data type to backfill (e.g., "dailies", "activities", "sleeps", "bodyComps")
|
|
138
610
|
* @param params - Time range for the backfill
|
|
139
611
|
*/
|
|
@@ -141,24 +613,54 @@ export class GarminClient {
|
|
|
141
613
|
summaryType: string,
|
|
142
614
|
params: TimeRangeParams,
|
|
143
615
|
): Promise<void> {
|
|
144
|
-
const query = timeRangeQuery(params);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
616
|
+
const query = timeRangeQuery(params, this.accessToken);
|
|
617
|
+
const qs = new URLSearchParams(query).toString();
|
|
618
|
+
const url = `${this.baseUrl}/wellness-api/rest/backfill/${summaryType}?${qs}`;
|
|
619
|
+
|
|
620
|
+
const response = await fetch(url, {
|
|
621
|
+
method: "GET",
|
|
622
|
+
headers: {
|
|
623
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
624
|
+
Accept: "application/json",
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (!response.ok) {
|
|
629
|
+
const body = await response.text().catch(() => "");
|
|
630
|
+
throw new GarminApiError(
|
|
631
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
632
|
+
response.status,
|
|
633
|
+
body,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
149
636
|
}
|
|
150
637
|
|
|
151
|
-
// ───
|
|
638
|
+
// ─── User Deregistration ──────────────────────────────────────────────
|
|
152
639
|
|
|
153
640
|
/**
|
|
154
|
-
*
|
|
641
|
+
* Delete the user's registration with Garmin.
|
|
155
642
|
*
|
|
156
|
-
*
|
|
643
|
+
* Must be called when the user disconnects or deletes their account
|
|
644
|
+
* to comply with Garmin's API requirements.
|
|
157
645
|
*/
|
|
158
|
-
async
|
|
159
|
-
|
|
646
|
+
async deleteUserRegistration(): Promise<void> {
|
|
647
|
+
const { error, response } = await this.wellness.DELETE(
|
|
648
|
+
"/rest/user/registration",
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
if (error) {
|
|
652
|
+
throw new GarminApiError(
|
|
653
|
+
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
654
|
+
response.status,
|
|
655
|
+
JSON.stringify(error),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
160
658
|
}
|
|
161
659
|
|
|
660
|
+
// ─── Training API V2 ─────────────────────────────────────────────────
|
|
661
|
+
// These endpoints are NOT part of the Wellness API spec, so they use
|
|
662
|
+
// manual fetch.
|
|
663
|
+
|
|
162
664
|
/**
|
|
163
665
|
* Create a workout in Garmin Connect.
|
|
164
666
|
*
|
|
@@ -243,34 +745,7 @@ export class GarminClient {
|
|
|
243
745
|
await this.del(`/training-api/schedule/${scheduleId}`);
|
|
244
746
|
}
|
|
245
747
|
|
|
246
|
-
// ───
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Delete the user's registration with Garmin.
|
|
250
|
-
*
|
|
251
|
-
* Must be called when the user disconnects or deletes their account
|
|
252
|
-
* to comply with Garmin's API requirements.
|
|
253
|
-
*/
|
|
254
|
-
async deleteUserRegistration(): Promise<void> {
|
|
255
|
-
const url = `${this.baseUrl}/wellness-api/rest/user/registration`;
|
|
256
|
-
const response = await fetch(url, {
|
|
257
|
-
method: "DELETE",
|
|
258
|
-
headers: {
|
|
259
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (!response.ok) {
|
|
264
|
-
const body = await response.text().catch(() => "");
|
|
265
|
-
throw new GarminApiError(
|
|
266
|
-
`Garmin API error: ${response.status} ${response.statusText}`,
|
|
267
|
-
response.status,
|
|
268
|
-
body,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ─── Internal ─────────────────────────────────────────────────────────
|
|
748
|
+
// ─── Internal (Training API helpers) ──────────────────────────────────
|
|
274
749
|
|
|
275
750
|
private async get<T>(
|
|
276
751
|
path: string,
|
|
@@ -386,10 +861,14 @@ export interface TimeRangeParams {
|
|
|
386
861
|
uploadEndTimeInSeconds: number;
|
|
387
862
|
}
|
|
388
863
|
|
|
389
|
-
function timeRangeQuery(
|
|
864
|
+
function timeRangeQuery(
|
|
865
|
+
params: TimeRangeParams,
|
|
866
|
+
token?: string,
|
|
867
|
+
): { uploadStartTimeInSeconds: string; uploadEndTimeInSeconds: string; token?: string } {
|
|
390
868
|
return {
|
|
391
869
|
uploadStartTimeInSeconds: String(params.uploadStartTimeInSeconds),
|
|
392
870
|
uploadEndTimeInSeconds: String(params.uploadEndTimeInSeconds),
|
|
871
|
+
...(token ? { token } : {}),
|
|
393
872
|
};
|
|
394
873
|
}
|
|
395
874
|
|
|
@@ -401,7 +880,7 @@ export class GarminApiError extends Error {
|
|
|
401
880
|
public readonly status: number,
|
|
402
881
|
public readonly body: string,
|
|
403
882
|
) {
|
|
404
|
-
super(message);
|
|
883
|
+
super(`${message} — ${body}`);
|
|
405
884
|
this.name = "GarminApiError";
|
|
406
885
|
}
|
|
407
886
|
}
|