@nativesquare/soma 0.16.2 → 0.16.4
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 +5 -85
- package/dist/client/healthkit.d.ts.map +1 -1
- package/dist/client/healthkit.js +14 -506
- 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/dist/component/validators/connection.js +1 -1
- package/dist/component/validators/connection.js.map +1 -1
- package/package.json +1 -1
- package/src/client/healthkit.ts +44 -625
- 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/component/validators/connection.ts +1 -1
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// ─── HealthKit Public Mutations ──────────────────────────────────────────────
|
|
2
|
+
// Per-data-type sync mutations plus connect/disconnect, called by the host app
|
|
3
|
+
// through the SomaHealthKit client class. Because HealthKit is an on-device
|
|
4
|
+
// provider, these are `mutation` (not `action`) — all data is supplied by the
|
|
5
|
+
// caller, nothing is fetched from an external API.
|
|
6
|
+
|
|
7
|
+
import { v } from "convex/values";
|
|
8
|
+
import { mutation } from "../_generated/server.js";
|
|
9
|
+
import type { MutationCtx } from "../_generated/server.js";
|
|
10
|
+
import type { Id } from "../_generated/dataModel.js";
|
|
11
|
+
import { api } from "../_generated/api.js";
|
|
12
|
+
import type { SomaError } from "../validators/shared.js";
|
|
13
|
+
import { transformWorkout } from "./transform/activity.js";
|
|
14
|
+
import { transformSleep } from "./transform/sleep.js";
|
|
15
|
+
import { transformBody } from "./transform/body.js";
|
|
16
|
+
import {
|
|
17
|
+
transformDaily,
|
|
18
|
+
transformDailyFromSummary,
|
|
19
|
+
} from "./transform/daily.js";
|
|
20
|
+
import { transformNutrition } from "./transform/nutrition.js";
|
|
21
|
+
import { transformMenstruation } from "./transform/menstruation.js";
|
|
22
|
+
import { transformAthlete } from "./transform/athlete.js";
|
|
23
|
+
import type {
|
|
24
|
+
HKWorkout,
|
|
25
|
+
HKCategorySample,
|
|
26
|
+
HKQuantitySample,
|
|
27
|
+
HKActivitySummary,
|
|
28
|
+
HKCharacteristics,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
import {
|
|
31
|
+
syncActivitiesArgs,
|
|
32
|
+
syncSleepArgs,
|
|
33
|
+
syncBodyArgs,
|
|
34
|
+
syncDailyArgs,
|
|
35
|
+
syncDailyFromSummaryArgs,
|
|
36
|
+
syncNutritionArgs,
|
|
37
|
+
syncMenstruationArgs,
|
|
38
|
+
syncAthleteArgs,
|
|
39
|
+
syncAllHealthKitArgs,
|
|
40
|
+
syncActivitiesReturn,
|
|
41
|
+
syncSleepReturn,
|
|
42
|
+
syncBodyReturn,
|
|
43
|
+
syncDailyReturn,
|
|
44
|
+
syncNutritionReturn,
|
|
45
|
+
syncMenstruationReturn,
|
|
46
|
+
syncAthleteReturn,
|
|
47
|
+
syncAllReturn,
|
|
48
|
+
} from "./validators.js";
|
|
49
|
+
|
|
50
|
+
const PROVIDER = "HEALTHKIT";
|
|
51
|
+
|
|
52
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load the active HEALTHKIT connection for a user, or throw if missing/inactive.
|
|
56
|
+
*
|
|
57
|
+
* HealthKit permissions are managed on-device, so the connection state is an
|
|
58
|
+
* assertion made by the host via {@link connect} / {@link disconnect}. Sync
|
|
59
|
+
* mutations refuse to run without an active connection.
|
|
60
|
+
*/
|
|
61
|
+
async function requireActiveConnection(
|
|
62
|
+
ctx: MutationCtx,
|
|
63
|
+
userId: string,
|
|
64
|
+
): Promise<Id<"connections">> {
|
|
65
|
+
const connection = await ctx.db
|
|
66
|
+
.query("connections")
|
|
67
|
+
.withIndex("by_userId_provider", (q) =>
|
|
68
|
+
q.eq("userId", userId).eq("provider", PROVIDER),
|
|
69
|
+
)
|
|
70
|
+
.first();
|
|
71
|
+
if (!connection) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`No HealthKit connection for user "${userId}". Call soma.healthkit.connect(ctx, { userId }) after the React Native HealthKit library confirms permissions.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (connection.active === false) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`HealthKit connection for user "${userId}" is disconnected. Call soma.healthkit.connect(ctx, { userId }) to re-activate.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return connection._id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function touchLastDataUpdate(
|
|
85
|
+
ctx: MutationCtx,
|
|
86
|
+
connectionId: Id<"connections">,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
89
|
+
connectionId,
|
|
90
|
+
lastDataUpdate: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Per-type processors (no connection resolution, no lastDataUpdate) ──────
|
|
95
|
+
|
|
96
|
+
async function processActivities(
|
|
97
|
+
ctx: MutationCtx,
|
|
98
|
+
connectionId: Id<"connections">,
|
|
99
|
+
userId: string,
|
|
100
|
+
workouts: HKWorkout[],
|
|
101
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
102
|
+
const errors: SomaError[] = [];
|
|
103
|
+
let count = 0;
|
|
104
|
+
for (const workout of workouts) {
|
|
105
|
+
try {
|
|
106
|
+
const data = transformWorkout(workout);
|
|
107
|
+
await ctx.runMutation(api.public.ingestActivity, {
|
|
108
|
+
connectionId,
|
|
109
|
+
userId,
|
|
110
|
+
...data,
|
|
111
|
+
});
|
|
112
|
+
count++;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
errors.push({
|
|
115
|
+
type: "activity",
|
|
116
|
+
id: workout.uuid,
|
|
117
|
+
message: err instanceof Error ? err.message : String(err),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { count, errors };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function processSleep(
|
|
125
|
+
ctx: MutationCtx,
|
|
126
|
+
connectionId: Id<"connections">,
|
|
127
|
+
userId: string,
|
|
128
|
+
sessions: HKCategorySample[][],
|
|
129
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
130
|
+
const errors: SomaError[] = [];
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (const session of sessions) {
|
|
133
|
+
const sessionId = session[0]?.uuid ?? "unknown";
|
|
134
|
+
try {
|
|
135
|
+
const data = transformSleep(session);
|
|
136
|
+
await ctx.runMutation(api.public.ingestSleep, {
|
|
137
|
+
connectionId,
|
|
138
|
+
userId,
|
|
139
|
+
...data,
|
|
140
|
+
});
|
|
141
|
+
count++;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
errors.push({
|
|
144
|
+
type: "sleep",
|
|
145
|
+
id: sessionId,
|
|
146
|
+
message: err instanceof Error ? err.message : String(err),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { count, errors };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function processBody(
|
|
154
|
+
ctx: MutationCtx,
|
|
155
|
+
connectionId: Id<"connections">,
|
|
156
|
+
userId: string,
|
|
157
|
+
samples: HKQuantitySample[],
|
|
158
|
+
timeRange?: { start_time: string; end_time: string },
|
|
159
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
160
|
+
const errors: SomaError[] = [];
|
|
161
|
+
let count = 0;
|
|
162
|
+
try {
|
|
163
|
+
const data = transformBody(samples, timeRange);
|
|
164
|
+
await ctx.runMutation(api.public.ingestBody, {
|
|
165
|
+
connectionId,
|
|
166
|
+
userId,
|
|
167
|
+
...data,
|
|
168
|
+
});
|
|
169
|
+
count++;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
errors.push({
|
|
172
|
+
type: "body",
|
|
173
|
+
id: "transform",
|
|
174
|
+
message: err instanceof Error ? err.message : String(err),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return { count, errors };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function processDaily(
|
|
181
|
+
ctx: MutationCtx,
|
|
182
|
+
connectionId: Id<"connections">,
|
|
183
|
+
userId: string,
|
|
184
|
+
samples: HKQuantitySample[],
|
|
185
|
+
timeRange?: { start_time: string; end_time: string },
|
|
186
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
187
|
+
const errors: SomaError[] = [];
|
|
188
|
+
let count = 0;
|
|
189
|
+
try {
|
|
190
|
+
const data = transformDaily(samples, timeRange);
|
|
191
|
+
await ctx.runMutation(api.public.ingestDaily, {
|
|
192
|
+
connectionId,
|
|
193
|
+
userId,
|
|
194
|
+
...data,
|
|
195
|
+
});
|
|
196
|
+
count++;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
errors.push({
|
|
199
|
+
type: "daily",
|
|
200
|
+
id: "transform",
|
|
201
|
+
message: err instanceof Error ? err.message : String(err),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return { count, errors };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function processDailyFromSummary(
|
|
208
|
+
ctx: MutationCtx,
|
|
209
|
+
connectionId: Id<"connections">,
|
|
210
|
+
userId: string,
|
|
211
|
+
summaries: HKActivitySummary[],
|
|
212
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
213
|
+
const errors: SomaError[] = [];
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const summary of summaries) {
|
|
216
|
+
const { year, month, day } = summary.dateComponents;
|
|
217
|
+
const summaryId = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
218
|
+
try {
|
|
219
|
+
const data = transformDailyFromSummary(summary);
|
|
220
|
+
await ctx.runMutation(api.public.ingestDaily, {
|
|
221
|
+
connectionId,
|
|
222
|
+
userId,
|
|
223
|
+
...data,
|
|
224
|
+
});
|
|
225
|
+
count++;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
errors.push({
|
|
228
|
+
type: "daily",
|
|
229
|
+
id: summaryId,
|
|
230
|
+
message: err instanceof Error ? err.message : String(err),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { count, errors };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function processNutrition(
|
|
238
|
+
ctx: MutationCtx,
|
|
239
|
+
connectionId: Id<"connections">,
|
|
240
|
+
userId: string,
|
|
241
|
+
samples: HKQuantitySample[],
|
|
242
|
+
timeRange?: { start_time: string; end_time: string },
|
|
243
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
244
|
+
const errors: SomaError[] = [];
|
|
245
|
+
let count = 0;
|
|
246
|
+
try {
|
|
247
|
+
const data = transformNutrition(samples, timeRange);
|
|
248
|
+
await ctx.runMutation(api.public.ingestNutrition, {
|
|
249
|
+
connectionId,
|
|
250
|
+
userId,
|
|
251
|
+
...data,
|
|
252
|
+
});
|
|
253
|
+
count++;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
errors.push({
|
|
256
|
+
type: "nutrition" as SomaError["type"],
|
|
257
|
+
id: "transform",
|
|
258
|
+
message: err instanceof Error ? err.message : String(err),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return { count, errors };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function processMenstruation(
|
|
265
|
+
ctx: MutationCtx,
|
|
266
|
+
connectionId: Id<"connections">,
|
|
267
|
+
userId: string,
|
|
268
|
+
samples: HKCategorySample[],
|
|
269
|
+
timeRange?: { start_time: string; end_time: string },
|
|
270
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
271
|
+
const errors: SomaError[] = [];
|
|
272
|
+
let count = 0;
|
|
273
|
+
try {
|
|
274
|
+
const data = transformMenstruation(samples, timeRange);
|
|
275
|
+
await ctx.runMutation(api.public.ingestMenstruation, {
|
|
276
|
+
connectionId,
|
|
277
|
+
userId,
|
|
278
|
+
...data,
|
|
279
|
+
});
|
|
280
|
+
count++;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
errors.push({
|
|
283
|
+
type: "menstruation",
|
|
284
|
+
id: "transform",
|
|
285
|
+
message: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return { count, errors };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function processAthlete(
|
|
292
|
+
ctx: MutationCtx,
|
|
293
|
+
connectionId: Id<"connections">,
|
|
294
|
+
userId: string,
|
|
295
|
+
characteristics: HKCharacteristics,
|
|
296
|
+
): Promise<{ count: number; errors: SomaError[] }> {
|
|
297
|
+
const errors: SomaError[] = [];
|
|
298
|
+
let count = 0;
|
|
299
|
+
try {
|
|
300
|
+
const data = transformAthlete(characteristics);
|
|
301
|
+
await ctx.runMutation(api.public.ingestAthlete, {
|
|
302
|
+
connectionId,
|
|
303
|
+
userId,
|
|
304
|
+
...data,
|
|
305
|
+
});
|
|
306
|
+
count++;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
errors.push({
|
|
309
|
+
type: "athlete",
|
|
310
|
+
id: "transform",
|
|
311
|
+
message: err instanceof Error ? err.message : String(err),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return { count, errors };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ─── Connect / Disconnect ────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Assert that HealthKit is connected for this user. Creates the HEALTHKIT
|
|
321
|
+
* connection if missing or re-activates it if previously disconnected.
|
|
322
|
+
* Idempotent.
|
|
323
|
+
*/
|
|
324
|
+
export const connect = mutation({
|
|
325
|
+
args: {
|
|
326
|
+
userId: v.string(),
|
|
327
|
+
},
|
|
328
|
+
returns: v.id("connections"),
|
|
329
|
+
handler: async (ctx, args): Promise<Id<"connections">> => {
|
|
330
|
+
return await ctx.runMutation(api.public.connect, {
|
|
331
|
+
userId: args.userId,
|
|
332
|
+
provider: PROVIDER,
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mark the HealthKit connection inactive for this user. Does not delete
|
|
339
|
+
* the connection row or any previously synced data.
|
|
340
|
+
*/
|
|
341
|
+
export const disconnect = mutation({
|
|
342
|
+
args: {
|
|
343
|
+
userId: v.string(),
|
|
344
|
+
},
|
|
345
|
+
returns: v.null(),
|
|
346
|
+
handler: async (ctx, args) => {
|
|
347
|
+
await ctx.runMutation(api.public.disconnect, {
|
|
348
|
+
userId: args.userId,
|
|
349
|
+
provider: PROVIDER,
|
|
350
|
+
});
|
|
351
|
+
return null;
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ─── Per-type sync mutations ─────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
export const syncActivities = mutation({
|
|
358
|
+
args: syncActivitiesArgs,
|
|
359
|
+
returns: syncActivitiesReturn,
|
|
360
|
+
handler: async (ctx, args) => {
|
|
361
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
362
|
+
const { count, errors } = await processActivities(
|
|
363
|
+
ctx,
|
|
364
|
+
connectionId,
|
|
365
|
+
args.userId,
|
|
366
|
+
args.workouts,
|
|
367
|
+
);
|
|
368
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
369
|
+
return { data: { activities: count }, errors };
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
export const syncSleep = mutation({
|
|
374
|
+
args: syncSleepArgs,
|
|
375
|
+
returns: syncSleepReturn,
|
|
376
|
+
handler: async (ctx, args) => {
|
|
377
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
378
|
+
const { count, errors } = await processSleep(
|
|
379
|
+
ctx,
|
|
380
|
+
connectionId,
|
|
381
|
+
args.userId,
|
|
382
|
+
args.sessions,
|
|
383
|
+
);
|
|
384
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
385
|
+
return { data: { sleep: count }, errors };
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
export const syncBody = mutation({
|
|
390
|
+
args: syncBodyArgs,
|
|
391
|
+
returns: syncBodyReturn,
|
|
392
|
+
handler: async (ctx, args) => {
|
|
393
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
394
|
+
const { count, errors } = await processBody(
|
|
395
|
+
ctx,
|
|
396
|
+
connectionId,
|
|
397
|
+
args.userId,
|
|
398
|
+
args.samples,
|
|
399
|
+
args.timeRange,
|
|
400
|
+
);
|
|
401
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
402
|
+
return { data: { body: count }, errors };
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export const syncDaily = mutation({
|
|
407
|
+
args: syncDailyArgs,
|
|
408
|
+
returns: syncDailyReturn,
|
|
409
|
+
handler: async (ctx, args) => {
|
|
410
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
411
|
+
const { count, errors } = await processDaily(
|
|
412
|
+
ctx,
|
|
413
|
+
connectionId,
|
|
414
|
+
args.userId,
|
|
415
|
+
args.samples,
|
|
416
|
+
args.timeRange,
|
|
417
|
+
);
|
|
418
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
419
|
+
return { data: { daily: count }, errors };
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
export const syncDailyFromSummary = mutation({
|
|
424
|
+
args: syncDailyFromSummaryArgs,
|
|
425
|
+
returns: syncDailyReturn,
|
|
426
|
+
handler: async (ctx, args) => {
|
|
427
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
428
|
+
const { count, errors } = await processDailyFromSummary(
|
|
429
|
+
ctx,
|
|
430
|
+
connectionId,
|
|
431
|
+
args.userId,
|
|
432
|
+
args.summaries,
|
|
433
|
+
);
|
|
434
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
435
|
+
return { data: { daily: count }, errors };
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
export const syncNutrition = mutation({
|
|
440
|
+
args: syncNutritionArgs,
|
|
441
|
+
returns: syncNutritionReturn,
|
|
442
|
+
handler: async (ctx, args) => {
|
|
443
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
444
|
+
const { count, errors } = await processNutrition(
|
|
445
|
+
ctx,
|
|
446
|
+
connectionId,
|
|
447
|
+
args.userId,
|
|
448
|
+
args.samples,
|
|
449
|
+
args.timeRange,
|
|
450
|
+
);
|
|
451
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
452
|
+
return { data: { nutrition: count }, errors };
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
export const syncMenstruation = mutation({
|
|
457
|
+
args: syncMenstruationArgs,
|
|
458
|
+
returns: syncMenstruationReturn,
|
|
459
|
+
handler: async (ctx, args) => {
|
|
460
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
461
|
+
const { count, errors } = await processMenstruation(
|
|
462
|
+
ctx,
|
|
463
|
+
connectionId,
|
|
464
|
+
args.userId,
|
|
465
|
+
args.samples,
|
|
466
|
+
args.timeRange,
|
|
467
|
+
);
|
|
468
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
469
|
+
return { data: { menstruation: count }, errors };
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
export const syncAthlete = mutation({
|
|
474
|
+
args: syncAthleteArgs,
|
|
475
|
+
returns: syncAthleteReturn,
|
|
476
|
+
handler: async (ctx, args) => {
|
|
477
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
478
|
+
const { count, errors } = await processAthlete(
|
|
479
|
+
ctx,
|
|
480
|
+
connectionId,
|
|
481
|
+
args.userId,
|
|
482
|
+
args.characteristics,
|
|
483
|
+
);
|
|
484
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
485
|
+
return { data: { athletes: count }, errors };
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// ─── Orchestrator ────────────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Sync all provided HealthKit data types in a single transaction.
|
|
493
|
+
*
|
|
494
|
+
* Only types with values provided are synced. Errors from individual data
|
|
495
|
+
* types are collected — one failing type does not block others. The
|
|
496
|
+
* connection is resolved once and `lastDataUpdate` is written once.
|
|
497
|
+
*/
|
|
498
|
+
export const syncAll = mutation({
|
|
499
|
+
args: syncAllHealthKitArgs,
|
|
500
|
+
returns: syncAllReturn,
|
|
501
|
+
handler: async (ctx, args) => {
|
|
502
|
+
const connectionId = await requireActiveConnection(ctx, args.userId);
|
|
503
|
+
const allErrors: SomaError[] = [];
|
|
504
|
+
const counts: Record<string, number> = {};
|
|
505
|
+
|
|
506
|
+
const record = (key: string, result: { count: number; errors: SomaError[] }) => {
|
|
507
|
+
counts[key] = (counts[key] ?? 0) + result.count;
|
|
508
|
+
allErrors.push(...result.errors);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
if (args.workouts) {
|
|
512
|
+
record(
|
|
513
|
+
"activities",
|
|
514
|
+
await processActivities(ctx, connectionId, args.userId, args.workouts),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (args.sleepSessions) {
|
|
518
|
+
record(
|
|
519
|
+
"sleep",
|
|
520
|
+
await processSleep(ctx, connectionId, args.userId, args.sleepSessions),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
if (args.bodySamples) {
|
|
524
|
+
record(
|
|
525
|
+
"body",
|
|
526
|
+
await processBody(
|
|
527
|
+
ctx,
|
|
528
|
+
connectionId,
|
|
529
|
+
args.userId,
|
|
530
|
+
args.bodySamples,
|
|
531
|
+
args.bodyTimeRange,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (args.dailySamples) {
|
|
536
|
+
record(
|
|
537
|
+
"daily",
|
|
538
|
+
await processDaily(
|
|
539
|
+
ctx,
|
|
540
|
+
connectionId,
|
|
541
|
+
args.userId,
|
|
542
|
+
args.dailySamples,
|
|
543
|
+
args.dailyTimeRange,
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (args.dailySummaries) {
|
|
548
|
+
record(
|
|
549
|
+
"daily",
|
|
550
|
+
await processDailyFromSummary(
|
|
551
|
+
ctx,
|
|
552
|
+
connectionId,
|
|
553
|
+
args.userId,
|
|
554
|
+
args.dailySummaries,
|
|
555
|
+
),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (args.nutritionSamples) {
|
|
559
|
+
record(
|
|
560
|
+
"nutrition",
|
|
561
|
+
await processNutrition(
|
|
562
|
+
ctx,
|
|
563
|
+
connectionId,
|
|
564
|
+
args.userId,
|
|
565
|
+
args.nutritionSamples,
|
|
566
|
+
args.nutritionTimeRange,
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (args.menstruationSamples) {
|
|
571
|
+
record(
|
|
572
|
+
"menstruation",
|
|
573
|
+
await processMenstruation(
|
|
574
|
+
ctx,
|
|
575
|
+
connectionId,
|
|
576
|
+
args.userId,
|
|
577
|
+
args.menstruationSamples,
|
|
578
|
+
args.menstruationTimeRange,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (args.characteristics) {
|
|
583
|
+
record(
|
|
584
|
+
"athletes",
|
|
585
|
+
await processAthlete(
|
|
586
|
+
ctx,
|
|
587
|
+
connectionId,
|
|
588
|
+
args.userId,
|
|
589
|
+
args.characteristics,
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await touchLastDataUpdate(ctx, connectionId);
|
|
595
|
+
return { data: counts, errors: allErrors };
|
|
596
|
+
},
|
|
597
|
+
});
|