@nativesquare/soma 0.16.2 → 0.16.3
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/healthkit.d.ts +4 -84
- package/dist/client/healthkit.d.ts.map +1 -1
- package/dist/client/healthkit.js +13 -505
- package/dist/client/healthkit.js.map +1 -1
- package/dist/component/_generated/api.d.ts +4 -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 +472 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/healthkit/index.d.ts +1 -0
- package/dist/component/healthkit/index.d.ts.map +1 -1
- package/dist/component/healthkit/index.js +5 -0
- package/dist/component/healthkit/index.js.map +1 -1
- package/dist/component/healthkit/public.d.ts +451 -0
- package/dist/component/healthkit/public.d.ts.map +1 -0
- package/dist/component/healthkit/public.js +386 -0
- package/dist/component/healthkit/public.js.map +1 -0
- package/dist/component/healthkit/types.d.ts +13 -76
- package/dist/component/healthkit/types.d.ts.map +1 -1
- package/dist/component/healthkit/types.js +9 -6
- package/dist/component/healthkit/types.js.map +1 -1
- package/dist/component/healthkit/validators.d.ts +1933 -0
- package/dist/component/healthkit/validators.d.ts.map +1 -0
- package/dist/component/healthkit/validators.js +199 -0
- package/dist/component/healthkit/validators.js.map +1 -0
- package/dist/component/strava/types/stravaApi/zod.gen.d.ts +3 -3
- package/package.json +1 -1
- package/src/client/healthkit.ts +43 -624
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +412 -0
- package/src/component/healthkit/index.ts +74 -46
- package/src/component/healthkit/public.ts +597 -0
- package/src/component/healthkit/types.ts +45 -114
- package/src/component/healthkit/validators.ts +253 -0
package/src/client/healthkit.ts
CHANGED
|
@@ -7,18 +7,6 @@ import type {
|
|
|
7
7
|
HKActivitySummary,
|
|
8
8
|
HKCharacteristics,
|
|
9
9
|
} from "../component/healthkit/types.js";
|
|
10
|
-
import { transformWorkout } from "../component/healthkit/transform/activity.js";
|
|
11
|
-
import { transformSleep } from "../component/healthkit/transform/sleep.js";
|
|
12
|
-
import { transformBody } from "../component/healthkit/transform/body.js";
|
|
13
|
-
import {
|
|
14
|
-
transformDaily,
|
|
15
|
-
transformDailyFromSummary,
|
|
16
|
-
} from "../component/healthkit/transform/daily.js";
|
|
17
|
-
import { transformNutrition } from "../component/healthkit/transform/nutrition.js";
|
|
18
|
-
import { transformMenstruation } from "../component/healthkit/transform/menstruation.js";
|
|
19
|
-
import { transformAthlete } from "../component/healthkit/transform/athlete.js";
|
|
20
|
-
|
|
21
|
-
const PROVIDER = "APPLE";
|
|
22
10
|
|
|
23
11
|
/**
|
|
24
12
|
* Client class for Apple HealthKit integration with Soma.
|
|
@@ -26,8 +14,8 @@ const PROVIDER = "APPLE";
|
|
|
26
14
|
* Unlike {@link import("./strava.js").SomaStrava | SomaStrava} and
|
|
27
15
|
* {@link import("./garmin.js").SomaGarmin | SomaGarmin}, HealthKit is an
|
|
28
16
|
* on-device provider — data is queried locally on iOS, not fetched from a
|
|
29
|
-
* cloud API. This class
|
|
30
|
-
*
|
|
17
|
+
* cloud API. This class is a thin wrapper over the component's public
|
|
18
|
+
* mutations, which own the transform + ingest pipeline.
|
|
31
19
|
*
|
|
32
20
|
* Because HealthKit permissions are managed in iOS Settings, the server
|
|
33
21
|
* cannot independently verify whether the user has granted or revoked
|
|
@@ -78,10 +66,6 @@ export class SomaHealthKit {
|
|
|
78
66
|
* permissions were granted. Creates the APPLE connection if missing,
|
|
79
67
|
* or re-activates it if previously disconnected. Idempotent.
|
|
80
68
|
*
|
|
81
|
-
* Because iOS permission grants happen on-device, the server cannot
|
|
82
|
-
* verify them independently — this method records the host app's
|
|
83
|
-
* assertion that the user has authorized HealthKit access.
|
|
84
|
-
*
|
|
85
69
|
* @param ctx - Mutation context from the host app
|
|
86
70
|
* @param args.userId - The host app's user identifier
|
|
87
71
|
* @returns The connection document ID
|
|
@@ -90,10 +74,7 @@ export class SomaHealthKit {
|
|
|
90
74
|
ctx: MutationCtx,
|
|
91
75
|
args: { userId: string },
|
|
92
76
|
): Promise<string> {
|
|
93
|
-
return
|
|
94
|
-
userId: args.userId,
|
|
95
|
-
provider: PROVIDER,
|
|
96
|
-
})) as string;
|
|
77
|
+
return await ctx.runMutation(this.component.healthkit.public.connect, args);
|
|
97
78
|
}
|
|
98
79
|
|
|
99
80
|
/**
|
|
@@ -114,10 +95,7 @@ export class SomaHealthKit {
|
|
|
114
95
|
ctx: MutationCtx,
|
|
115
96
|
args: { userId: string },
|
|
116
97
|
): Promise<null> {
|
|
117
|
-
return await ctx.runMutation(this.component.public.disconnect,
|
|
118
|
-
userId: args.userId,
|
|
119
|
-
provider: PROVIDER,
|
|
120
|
-
});
|
|
98
|
+
return await ctx.runMutation(this.component.healthkit.public.disconnect, args);
|
|
121
99
|
}
|
|
122
100
|
|
|
123
101
|
// ─── Per-Type Sync Methods ─────────────────────────────────────────────
|
|
@@ -127,39 +105,15 @@ export class SomaHealthKit {
|
|
|
127
105
|
*
|
|
128
106
|
* Transforms each `HKWorkout` into the Soma Activity schema and ingests it
|
|
129
107
|
* with automatic deduplication by `workout.uuid`.
|
|
130
|
-
*
|
|
131
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
132
|
-
* @param args.userId - The host app's user identifier
|
|
133
|
-
* @param args.workouts - Array of HKWorkout objects from HealthKit
|
|
134
108
|
*/
|
|
135
109
|
async syncActivities(
|
|
136
110
|
ctx: MutationCtx,
|
|
137
111
|
args: { userId: string; workouts: HKWorkout[] },
|
|
138
112
|
): Promise<SomaResult<{ activities: number }>> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const workout of args.workouts) {
|
|
144
|
-
try {
|
|
145
|
-
const data = transformWorkout(workout);
|
|
146
|
-
await ctx.runMutation(this.component.public.ingestActivity, {
|
|
147
|
-
connectionId,
|
|
148
|
-
userId: args.userId,
|
|
149
|
-
...data,
|
|
150
|
-
});
|
|
151
|
-
count++;
|
|
152
|
-
} catch (err) {
|
|
153
|
-
errors.push({
|
|
154
|
-
type: "activity",
|
|
155
|
-
id: workout.uuid,
|
|
156
|
-
message: err instanceof Error ? err.message : String(err),
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
162
|
-
return { data: { activities: count }, errors };
|
|
113
|
+
return (await ctx.runMutation(
|
|
114
|
+
this.component.healthkit.public.syncActivities,
|
|
115
|
+
args,
|
|
116
|
+
)) as SomaResult<{ activities: number }>;
|
|
163
117
|
}
|
|
164
118
|
|
|
165
119
|
/**
|
|
@@ -168,40 +122,15 @@ export class SomaHealthKit {
|
|
|
168
122
|
* Each inner array represents one sleep session (all `HKCategorySample`
|
|
169
123
|
* stage records for a single night). Each session is aggregated into a
|
|
170
124
|
* single Soma Sleep document.
|
|
171
|
-
*
|
|
172
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
173
|
-
* @param args.userId - The host app's user identifier
|
|
174
|
-
* @param args.sessions - Array of sessions, each an array of sleep-stage samples
|
|
175
125
|
*/
|
|
176
126
|
async syncSleep(
|
|
177
127
|
ctx: MutationCtx,
|
|
178
128
|
args: { userId: string; sessions: HKCategorySample[][] },
|
|
179
129
|
): Promise<SomaResult<{ sleep: number }>> {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const session of args.sessions) {
|
|
185
|
-
const sessionId = session[0]?.uuid ?? "unknown";
|
|
186
|
-
try {
|
|
187
|
-
const data = transformSleep(session);
|
|
188
|
-
await ctx.runMutation(this.component.public.ingestSleep, {
|
|
189
|
-
connectionId,
|
|
190
|
-
userId: args.userId,
|
|
191
|
-
...data,
|
|
192
|
-
});
|
|
193
|
-
count++;
|
|
194
|
-
} catch (err) {
|
|
195
|
-
errors.push({
|
|
196
|
-
type: "sleep",
|
|
197
|
-
id: sessionId,
|
|
198
|
-
message: err instanceof Error ? err.message : String(err),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
204
|
-
return { data: { sleep: count }, errors };
|
|
130
|
+
return (await ctx.runMutation(
|
|
131
|
+
this.component.healthkit.public.syncSleep,
|
|
132
|
+
args,
|
|
133
|
+
)) as SomaResult<{ sleep: number }>;
|
|
205
134
|
}
|
|
206
135
|
|
|
207
136
|
/**
|
|
@@ -210,11 +139,6 @@ export class SomaHealthKit {
|
|
|
210
139
|
* Accepts a mixed array of body-related quantity samples (heart rate, HRV,
|
|
211
140
|
* blood pressure, SpO2, weight, etc.) for a single time window and produces
|
|
212
141
|
* one Soma Body document.
|
|
213
|
-
*
|
|
214
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
215
|
-
* @param args.userId - The host app's user identifier
|
|
216
|
-
* @param args.samples - Array of HKQuantitySample for the desired time range
|
|
217
|
-
* @param args.timeRange - Optional explicit time range; auto-detected from samples if omitted
|
|
218
142
|
*/
|
|
219
143
|
async syncBody(
|
|
220
144
|
ctx: MutationCtx,
|
|
@@ -224,40 +148,14 @@ export class SomaHealthKit {
|
|
|
224
148
|
timeRange?: { start_time: string; end_time: string };
|
|
225
149
|
},
|
|
226
150
|
): Promise<SomaResult<{ body: number }>> {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const data = transformBody(args.samples, args.timeRange);
|
|
233
|
-
await ctx.runMutation(this.component.public.ingestBody, {
|
|
234
|
-
connectionId,
|
|
235
|
-
userId: args.userId,
|
|
236
|
-
...data,
|
|
237
|
-
});
|
|
238
|
-
count++;
|
|
239
|
-
} catch (err) {
|
|
240
|
-
errors.push({
|
|
241
|
-
type: "body",
|
|
242
|
-
id: "transform",
|
|
243
|
-
message: err instanceof Error ? err.message : String(err),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
248
|
-
return { data: { body: count }, errors };
|
|
151
|
+
return (await ctx.runMutation(
|
|
152
|
+
this.component.healthkit.public.syncBody,
|
|
153
|
+
args,
|
|
154
|
+
)) as SomaResult<{ body: number }>;
|
|
249
155
|
}
|
|
250
156
|
|
|
251
157
|
/**
|
|
252
158
|
* Sync daily activity data from HealthKit quantity samples.
|
|
253
|
-
*
|
|
254
|
-
* Accepts samples for a single day (steps, distance, energy, exercise time,
|
|
255
|
-
* heart rate, etc.) and produces one Soma Daily document.
|
|
256
|
-
*
|
|
257
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
258
|
-
* @param args.userId - The host app's user identifier
|
|
259
|
-
* @param args.samples - Array of HKQuantitySample for the desired day
|
|
260
|
-
* @param args.timeRange - Optional explicit time range; auto-detected from samples if omitted
|
|
261
159
|
*/
|
|
262
160
|
async syncDaily(
|
|
263
161
|
ctx: MutationCtx,
|
|
@@ -267,82 +165,27 @@ export class SomaHealthKit {
|
|
|
267
165
|
timeRange?: { start_time: string; end_time: string };
|
|
268
166
|
},
|
|
269
167
|
): Promise<SomaResult<{ daily: number }>> {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const data = transformDaily(args.samples, args.timeRange);
|
|
276
|
-
await ctx.runMutation(this.component.public.ingestDaily, {
|
|
277
|
-
connectionId,
|
|
278
|
-
userId: args.userId,
|
|
279
|
-
...data,
|
|
280
|
-
});
|
|
281
|
-
count++;
|
|
282
|
-
} catch (err) {
|
|
283
|
-
errors.push({
|
|
284
|
-
type: "daily",
|
|
285
|
-
id: "transform",
|
|
286
|
-
message: err instanceof Error ? err.message : String(err),
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
291
|
-
return { data: { daily: count }, errors };
|
|
168
|
+
return (await ctx.runMutation(
|
|
169
|
+
this.component.healthkit.public.syncDaily,
|
|
170
|
+
args,
|
|
171
|
+
)) as SomaResult<{ daily: number }>;
|
|
292
172
|
}
|
|
293
173
|
|
|
294
174
|
/**
|
|
295
175
|
* Sync daily activity data from HealthKit activity ring summaries.
|
|
296
|
-
*
|
|
297
|
-
* Each `HKActivitySummary` represents one day's activity rings (Move,
|
|
298
|
-
* Exercise, Stand). Each summary produces one Soma Daily document.
|
|
299
|
-
*
|
|
300
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
301
|
-
* @param args.userId - The host app's user identifier
|
|
302
|
-
* @param args.summaries - Array of HKActivitySummary from HealthKit
|
|
303
176
|
*/
|
|
304
177
|
async syncDailyFromSummary(
|
|
305
178
|
ctx: MutationCtx,
|
|
306
179
|
args: { userId: string; summaries: HKActivitySummary[] },
|
|
307
180
|
): Promise<SomaResult<{ daily: number }>> {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (const summary of args.summaries) {
|
|
313
|
-
const { year, month, day } = summary.dateComponents;
|
|
314
|
-
const summaryId = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
315
|
-
try {
|
|
316
|
-
const data = transformDailyFromSummary(summary);
|
|
317
|
-
await ctx.runMutation(this.component.public.ingestDaily, {
|
|
318
|
-
connectionId,
|
|
319
|
-
userId: args.userId,
|
|
320
|
-
...data,
|
|
321
|
-
});
|
|
322
|
-
count++;
|
|
323
|
-
} catch (err) {
|
|
324
|
-
errors.push({
|
|
325
|
-
type: "daily",
|
|
326
|
-
id: summaryId,
|
|
327
|
-
message: err instanceof Error ? err.message : String(err),
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
333
|
-
return { data: { daily: count }, errors };
|
|
181
|
+
return (await ctx.runMutation(
|
|
182
|
+
this.component.healthkit.public.syncDailyFromSummary,
|
|
183
|
+
args,
|
|
184
|
+
)) as SomaResult<{ daily: number }>;
|
|
334
185
|
}
|
|
335
186
|
|
|
336
187
|
/**
|
|
337
188
|
* Sync nutrition data from HealthKit.
|
|
338
|
-
*
|
|
339
|
-
* Accepts dietary quantity samples for a single time window and produces
|
|
340
|
-
* one Soma Nutrition document with macros and micros.
|
|
341
|
-
*
|
|
342
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
343
|
-
* @param args.userId - The host app's user identifier
|
|
344
|
-
* @param args.samples - Array of HKQuantitySample with dietary type identifiers
|
|
345
|
-
* @param args.timeRange - Optional explicit time range; auto-detected from samples if omitted
|
|
346
189
|
*/
|
|
347
190
|
async syncNutrition(
|
|
348
191
|
ctx: MutationCtx,
|
|
@@ -352,40 +195,14 @@ export class SomaHealthKit {
|
|
|
352
195
|
timeRange?: { start_time: string; end_time: string };
|
|
353
196
|
},
|
|
354
197
|
): Promise<SomaResult<{ nutrition: number }>> {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
const data = transformNutrition(args.samples, args.timeRange);
|
|
361
|
-
await ctx.runMutation(this.component.public.ingestNutrition, {
|
|
362
|
-
connectionId,
|
|
363
|
-
userId: args.userId,
|
|
364
|
-
...data,
|
|
365
|
-
});
|
|
366
|
-
count++;
|
|
367
|
-
} catch (err) {
|
|
368
|
-
errors.push({
|
|
369
|
-
type: "nutrition" as SomaError["type"],
|
|
370
|
-
id: "transform",
|
|
371
|
-
message: err instanceof Error ? err.message : String(err),
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
376
|
-
return { data: { nutrition: count }, errors };
|
|
198
|
+
return (await ctx.runMutation(
|
|
199
|
+
this.component.healthkit.public.syncNutrition,
|
|
200
|
+
args,
|
|
201
|
+
)) as SomaResult<{ nutrition: number }>;
|
|
377
202
|
}
|
|
378
203
|
|
|
379
204
|
/**
|
|
380
205
|
* Sync menstruation data from HealthKit.
|
|
381
|
-
*
|
|
382
|
-
* Accepts menstrual flow category samples for a single time window and
|
|
383
|
-
* produces one Soma Menstruation document.
|
|
384
|
-
*
|
|
385
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
386
|
-
* @param args.userId - The host app's user identifier
|
|
387
|
-
* @param args.samples - Array of HKCategorySample with menstrual flow values
|
|
388
|
-
* @param args.timeRange - Optional explicit time range; auto-detected from samples if omitted
|
|
389
206
|
*/
|
|
390
207
|
async syncMenstruation(
|
|
391
208
|
ctx: MutationCtx,
|
|
@@ -395,28 +212,10 @@ export class SomaHealthKit {
|
|
|
395
212
|
timeRange?: { start_time: string; end_time: string };
|
|
396
213
|
},
|
|
397
214
|
): Promise<SomaResult<{ menstruation: number }>> {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
const data = transformMenstruation(args.samples, args.timeRange);
|
|
404
|
-
await ctx.runMutation(this.component.public.ingestMenstruation, {
|
|
405
|
-
connectionId,
|
|
406
|
-
userId: args.userId,
|
|
407
|
-
...data,
|
|
408
|
-
});
|
|
409
|
-
count++;
|
|
410
|
-
} catch (err) {
|
|
411
|
-
errors.push({
|
|
412
|
-
type: "menstruation",
|
|
413
|
-
id: "transform",
|
|
414
|
-
message: err instanceof Error ? err.message : String(err),
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
419
|
-
return { data: { menstruation: count }, errors };
|
|
215
|
+
return (await ctx.runMutation(
|
|
216
|
+
this.component.healthkit.public.syncMenstruation,
|
|
217
|
+
args,
|
|
218
|
+
)) as SomaResult<{ menstruation: number }>;
|
|
420
219
|
}
|
|
421
220
|
|
|
422
221
|
/**
|
|
@@ -424,37 +223,15 @@ export class SomaHealthKit {
|
|
|
424
223
|
*
|
|
425
224
|
* HealthKit exposes limited profile data (biological sex, date of birth).
|
|
426
225
|
* Produces one Soma Athlete document per connection.
|
|
427
|
-
*
|
|
428
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
429
|
-
* @param args.userId - The host app's user identifier
|
|
430
|
-
* @param args.characteristics - The HKCharacteristics from HealthKit
|
|
431
226
|
*/
|
|
432
227
|
async syncAthlete(
|
|
433
228
|
ctx: MutationCtx,
|
|
434
229
|
args: { userId: string; characteristics: HKCharacteristics },
|
|
435
230
|
): Promise<SomaResult<{ athletes: number }>> {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
const data = transformAthlete(args.characteristics);
|
|
442
|
-
await ctx.runMutation(this.component.public.ingestAthlete, {
|
|
443
|
-
connectionId,
|
|
444
|
-
userId: args.userId,
|
|
445
|
-
...data,
|
|
446
|
-
});
|
|
447
|
-
count++;
|
|
448
|
-
} catch (err) {
|
|
449
|
-
errors.push({
|
|
450
|
-
type: "athlete",
|
|
451
|
-
id: "transform",
|
|
452
|
-
message: err instanceof Error ? err.message : String(err),
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
457
|
-
return { data: { athletes: count }, errors };
|
|
231
|
+
return (await ctx.runMutation(
|
|
232
|
+
this.component.healthkit.public.syncAthlete,
|
|
233
|
+
args,
|
|
234
|
+
)) as SomaResult<{ athletes: number }>;
|
|
458
235
|
}
|
|
459
236
|
|
|
460
237
|
// ─── Orchestrator ────────────────────────────────────────────────────────
|
|
@@ -465,17 +242,6 @@ export class SomaHealthKit {
|
|
|
465
242
|
* Only data types with values provided are synced. Errors from individual
|
|
466
243
|
* data types are collected — one failing type does not block others.
|
|
467
244
|
*
|
|
468
|
-
* @param ctx - Mutation (or action) context from the host app
|
|
469
|
-
* @param args.userId - The host app's user identifier
|
|
470
|
-
* @param args.workouts - Optional array of HKWorkout objects
|
|
471
|
-
* @param args.sleepSessions - Optional array of sleep sessions (each an array of stage samples)
|
|
472
|
-
* @param args.bodySamples - Optional body-related quantity samples
|
|
473
|
-
* @param args.dailySamples - Optional daily activity quantity samples
|
|
474
|
-
* @param args.dailySummaries - Optional daily activity ring summaries
|
|
475
|
-
* @param args.nutritionSamples - Optional dietary quantity samples
|
|
476
|
-
* @param args.menstruationSamples - Optional menstrual flow category samples
|
|
477
|
-
* @param args.characteristics - Optional user characteristics
|
|
478
|
-
*
|
|
479
245
|
* @example
|
|
480
246
|
* ```ts
|
|
481
247
|
* const result = await soma.healthkit.syncAll(ctx, {
|
|
@@ -506,359 +272,12 @@ export class SomaHealthKit {
|
|
|
506
272
|
characteristics?: HKCharacteristics;
|
|
507
273
|
},
|
|
508
274
|
): Promise<SomaResult<Record<string, number>>> {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const run = async <T extends Record<string, number>>(
|
|
514
|
-
fn: () => Promise<SomaResult<T>>,
|
|
515
|
-
fallbackType: SomaError["type"],
|
|
516
|
-
) => {
|
|
517
|
-
try {
|
|
518
|
-
const result = await fn();
|
|
519
|
-
Object.assign(counts, result.data);
|
|
520
|
-
allErrors.push(...result.errors);
|
|
521
|
-
} catch (err) {
|
|
522
|
-
allErrors.push({
|
|
523
|
-
type: fallbackType,
|
|
524
|
-
id: "sync",
|
|
525
|
-
message: err instanceof Error ? err.message : String(err),
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
if (args.workouts) {
|
|
531
|
-
await run(
|
|
532
|
-
() => this.syncActivitiesInternal(ctx, connectionId, args.userId, args.workouts!),
|
|
533
|
-
"activity",
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
if (args.sleepSessions) {
|
|
537
|
-
await run(
|
|
538
|
-
() => this.syncSleepInternal(ctx, connectionId, args.userId, args.sleepSessions!),
|
|
539
|
-
"sleep",
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
if (args.bodySamples) {
|
|
543
|
-
await run(
|
|
544
|
-
() => this.syncBodyInternal(ctx, connectionId, args.userId, args.bodySamples!, args.bodyTimeRange),
|
|
545
|
-
"body",
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
if (args.dailySamples) {
|
|
549
|
-
await run(
|
|
550
|
-
() => this.syncDailyInternal(ctx, connectionId, args.userId, args.dailySamples!, args.dailyTimeRange),
|
|
551
|
-
"daily",
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
if (args.dailySummaries) {
|
|
555
|
-
await run(
|
|
556
|
-
() => this.syncDailyFromSummaryInternal(ctx, connectionId, args.userId, args.dailySummaries!),
|
|
557
|
-
"daily",
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
if (args.nutritionSamples) {
|
|
561
|
-
await run(
|
|
562
|
-
() => this.syncNutritionInternal(ctx, connectionId, args.userId, args.nutritionSamples!, args.nutritionTimeRange),
|
|
563
|
-
"nutrition" as SomaError["type"],
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
if (args.menstruationSamples) {
|
|
567
|
-
await run(
|
|
568
|
-
() => this.syncMenstruationInternal(ctx, connectionId, args.userId, args.menstruationSamples!, args.menstruationTimeRange),
|
|
569
|
-
"menstruation",
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
if (args.characteristics) {
|
|
573
|
-
await run(
|
|
574
|
-
() => this.syncAthleteInternal(ctx, connectionId, args.userId, args.characteristics!),
|
|
575
|
-
"athlete",
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Update lastDataUpdate once at the end
|
|
580
|
-
await this.updateLastDataUpdate(ctx, connectionId);
|
|
581
|
-
|
|
582
|
-
return { data: counts, errors: allErrors };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// ─── Private Helpers ───────────��─────────────────────────────────────────
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Load the active APPLE connection for a user, or throw if missing/inactive.
|
|
589
|
-
*
|
|
590
|
-
* HealthKit permissions are managed on-device, so the connection state is
|
|
591
|
-
* an assertion made by the host app via {@link SomaHealthKit.connect} and
|
|
592
|
-
* {@link SomaHealthKit.disconnect}. Sync methods refuse to run without an
|
|
593
|
-
* active connection — the host app must explicitly reconnect to resume.
|
|
594
|
-
*/
|
|
595
|
-
private async requireActiveConnection(
|
|
596
|
-
ctx: MutationCtx,
|
|
597
|
-
userId: string,
|
|
598
|
-
): Promise<string> {
|
|
599
|
-
const connection = await ctx.runQuery(
|
|
600
|
-
this.component.public.getConnectionByProvider,
|
|
601
|
-
{ userId, provider: PROVIDER },
|
|
602
|
-
);
|
|
603
|
-
if (!connection) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
`No HealthKit connection for user "${userId}". Call soma.healthkit.connect(ctx, { userId }) after the React Native HealthKit library confirms permissions.`,
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
if (connection.active === false) {
|
|
609
|
-
throw new Error(
|
|
610
|
-
`HealthKit connection for user "${userId}" is disconnected. Call soma.healthkit.connect(ctx, { userId }) to re-activate.`,
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
return connection._id;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
private async updateLastDataUpdate(
|
|
617
|
-
ctx: MutationCtx,
|
|
618
|
-
connectionId: string,
|
|
619
|
-
): Promise<void> {
|
|
620
|
-
await ctx.runMutation(this.component.public.updateConnection, {
|
|
621
|
-
connectionId,
|
|
622
|
-
lastDataUpdate: new Date().toISOString(),
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ─── Internal sync methods (skip connection resolution + lastDataUpdate) ─
|
|
627
|
-
|
|
628
|
-
private async syncActivitiesInternal(
|
|
629
|
-
ctx: MutationCtx,
|
|
630
|
-
connectionId: string,
|
|
631
|
-
userId: string,
|
|
632
|
-
workouts: HKWorkout[],
|
|
633
|
-
): Promise<SomaResult<{ activities: number }>> {
|
|
634
|
-
const errors: SomaError[] = [];
|
|
635
|
-
let count = 0;
|
|
636
|
-
|
|
637
|
-
for (const workout of workouts) {
|
|
638
|
-
try {
|
|
639
|
-
const data = transformWorkout(workout);
|
|
640
|
-
await ctx.runMutation(this.component.public.ingestActivity, {
|
|
641
|
-
connectionId,
|
|
642
|
-
userId,
|
|
643
|
-
...data,
|
|
644
|
-
});
|
|
645
|
-
count++;
|
|
646
|
-
} catch (err) {
|
|
647
|
-
errors.push({
|
|
648
|
-
type: "activity",
|
|
649
|
-
id: workout.uuid,
|
|
650
|
-
message: err instanceof Error ? err.message : String(err),
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return { data: { activities: count }, errors };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
private async syncSleepInternal(
|
|
659
|
-
ctx: MutationCtx,
|
|
660
|
-
connectionId: string,
|
|
661
|
-
userId: string,
|
|
662
|
-
sessions: HKCategorySample[][],
|
|
663
|
-
): Promise<SomaResult<{ sleep: number }>> {
|
|
664
|
-
const errors: SomaError[] = [];
|
|
665
|
-
let count = 0;
|
|
666
|
-
|
|
667
|
-
for (const session of sessions) {
|
|
668
|
-
const sessionId = session[0]?.uuid ?? "unknown";
|
|
669
|
-
try {
|
|
670
|
-
const data = transformSleep(session);
|
|
671
|
-
await ctx.runMutation(this.component.public.ingestSleep, {
|
|
672
|
-
connectionId,
|
|
673
|
-
userId,
|
|
674
|
-
...data,
|
|
675
|
-
});
|
|
676
|
-
count++;
|
|
677
|
-
} catch (err) {
|
|
678
|
-
errors.push({
|
|
679
|
-
type: "sleep",
|
|
680
|
-
id: sessionId,
|
|
681
|
-
message: err instanceof Error ? err.message : String(err),
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return { data: { sleep: count }, errors };
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
private async syncBodyInternal(
|
|
690
|
-
ctx: MutationCtx,
|
|
691
|
-
connectionId: string,
|
|
692
|
-
userId: string,
|
|
693
|
-
samples: HKQuantitySample[],
|
|
694
|
-
timeRange?: { start_time: string; end_time: string },
|
|
695
|
-
): Promise<SomaResult<{ body: number }>> {
|
|
696
|
-
const errors: SomaError[] = [];
|
|
697
|
-
let count = 0;
|
|
698
|
-
|
|
699
|
-
try {
|
|
700
|
-
const data = transformBody(samples, timeRange);
|
|
701
|
-
await ctx.runMutation(this.component.public.ingestBody, {
|
|
702
|
-
connectionId,
|
|
703
|
-
userId,
|
|
704
|
-
...data,
|
|
705
|
-
});
|
|
706
|
-
count++;
|
|
707
|
-
} catch (err) {
|
|
708
|
-
errors.push({
|
|
709
|
-
type: "body",
|
|
710
|
-
id: "transform",
|
|
711
|
-
message: err instanceof Error ? err.message : String(err),
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return { data: { body: count }, errors };
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
private async syncDailyInternal(
|
|
719
|
-
ctx: MutationCtx,
|
|
720
|
-
connectionId: string,
|
|
721
|
-
userId: string,
|
|
722
|
-
samples: HKQuantitySample[],
|
|
723
|
-
timeRange?: { start_time: string; end_time: string },
|
|
724
|
-
): Promise<SomaResult<{ daily: number }>> {
|
|
725
|
-
const errors: SomaError[] = [];
|
|
726
|
-
let count = 0;
|
|
727
|
-
|
|
728
|
-
try {
|
|
729
|
-
const data = transformDaily(samples, timeRange);
|
|
730
|
-
await ctx.runMutation(this.component.public.ingestDaily, {
|
|
731
|
-
connectionId,
|
|
732
|
-
userId,
|
|
733
|
-
...data,
|
|
734
|
-
});
|
|
735
|
-
count++;
|
|
736
|
-
} catch (err) {
|
|
737
|
-
errors.push({
|
|
738
|
-
type: "daily",
|
|
739
|
-
id: "transform",
|
|
740
|
-
message: err instanceof Error ? err.message : String(err),
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return { data: { daily: count }, errors };
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
private async syncDailyFromSummaryInternal(
|
|
748
|
-
ctx: MutationCtx,
|
|
749
|
-
connectionId: string,
|
|
750
|
-
userId: string,
|
|
751
|
-
summaries: HKActivitySummary[],
|
|
752
|
-
): Promise<SomaResult<{ daily: number }>> {
|
|
753
|
-
const errors: SomaError[] = [];
|
|
754
|
-
let count = 0;
|
|
755
|
-
|
|
756
|
-
for (const summary of summaries) {
|
|
757
|
-
const { year, month, day } = summary.dateComponents;
|
|
758
|
-
const summaryId = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
759
|
-
try {
|
|
760
|
-
const data = transformDailyFromSummary(summary);
|
|
761
|
-
await ctx.runMutation(this.component.public.ingestDaily, {
|
|
762
|
-
connectionId,
|
|
763
|
-
userId,
|
|
764
|
-
...data,
|
|
765
|
-
});
|
|
766
|
-
count++;
|
|
767
|
-
} catch (err) {
|
|
768
|
-
errors.push({
|
|
769
|
-
type: "daily",
|
|
770
|
-
id: summaryId,
|
|
771
|
-
message: err instanceof Error ? err.message : String(err),
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
return { data: { daily: count }, errors };
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
private async syncNutritionInternal(
|
|
780
|
-
ctx: MutationCtx,
|
|
781
|
-
connectionId: string,
|
|
782
|
-
userId: string,
|
|
783
|
-
samples: HKQuantitySample[],
|
|
784
|
-
timeRange?: { start_time: string; end_time: string },
|
|
785
|
-
): Promise<SomaResult<{ nutrition: number }>> {
|
|
786
|
-
const errors: SomaError[] = [];
|
|
787
|
-
let count = 0;
|
|
788
|
-
|
|
789
|
-
try {
|
|
790
|
-
const data = transformNutrition(samples, timeRange);
|
|
791
|
-
await ctx.runMutation(this.component.public.ingestNutrition, {
|
|
792
|
-
connectionId,
|
|
793
|
-
userId,
|
|
794
|
-
...data,
|
|
795
|
-
});
|
|
796
|
-
count++;
|
|
797
|
-
} catch (err) {
|
|
798
|
-
errors.push({
|
|
799
|
-
type: "nutrition" as SomaError["type"],
|
|
800
|
-
id: "transform",
|
|
801
|
-
message: err instanceof Error ? err.message : String(err),
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return { data: { nutrition: count }, errors };
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
private async syncMenstruationInternal(
|
|
809
|
-
ctx: MutationCtx,
|
|
810
|
-
connectionId: string,
|
|
811
|
-
userId: string,
|
|
812
|
-
samples: HKCategorySample[],
|
|
813
|
-
timeRange?: { start_time: string; end_time: string },
|
|
814
|
-
): Promise<SomaResult<{ menstruation: number }>> {
|
|
815
|
-
const errors: SomaError[] = [];
|
|
816
|
-
let count = 0;
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
const data = transformMenstruation(samples, timeRange);
|
|
820
|
-
await ctx.runMutation(this.component.public.ingestMenstruation, {
|
|
821
|
-
connectionId,
|
|
822
|
-
userId,
|
|
823
|
-
...data,
|
|
824
|
-
});
|
|
825
|
-
count++;
|
|
826
|
-
} catch (err) {
|
|
827
|
-
errors.push({
|
|
828
|
-
type: "menstruation",
|
|
829
|
-
id: "transform",
|
|
830
|
-
message: err instanceof Error ? err.message : String(err),
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
return { data: { menstruation: count }, errors };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
private async syncAthleteInternal(
|
|
838
|
-
ctx: MutationCtx,
|
|
839
|
-
connectionId: string,
|
|
840
|
-
userId: string,
|
|
841
|
-
characteristics: HKCharacteristics,
|
|
842
|
-
): Promise<SomaResult<{ athletes: number }>> {
|
|
843
|
-
const errors: SomaError[] = [];
|
|
844
|
-
let count = 0;
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
const data = transformAthlete(characteristics);
|
|
848
|
-
await ctx.runMutation(this.component.public.ingestAthlete, {
|
|
849
|
-
connectionId,
|
|
850
|
-
userId,
|
|
851
|
-
...data,
|
|
852
|
-
});
|
|
853
|
-
count++;
|
|
854
|
-
} catch (err) {
|
|
855
|
-
errors.push({
|
|
856
|
-
type: "athlete",
|
|
857
|
-
id: "transform",
|
|
858
|
-
message: err instanceof Error ? err.message : String(err),
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return { data: { athletes: count }, errors };
|
|
275
|
+
return (await ctx.runMutation(
|
|
276
|
+
this.component.healthkit.public.syncAll,
|
|
277
|
+
args,
|
|
278
|
+
)) as SomaResult<Record<string, number>>;
|
|
863
279
|
}
|
|
864
280
|
}
|
|
281
|
+
|
|
282
|
+
// Re-export for JSDoc link resolution above.
|
|
283
|
+
export type { SomaError };
|