@nativesquare/soma 0.3.0 → 0.4.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 +167 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +150 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -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 +56 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +110 -0
- package/dist/component/garmin.d.ts.map +1 -0
- package/dist/component/garmin.js +454 -0
- package/dist/component/garmin.js.map +1 -0
- package/dist/component/public.d.ts +761 -761
- package/dist/component/schema.d.ts +390 -388
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +5 -4
- package/dist/component/strava.d.ts.map +1 -1
- package/dist/component/strava.js +18 -1
- package/dist/component/strava.js.map +1 -1
- package/dist/component/validators/activity.d.ts +42 -42
- package/dist/component/validators/body.d.ts +47 -47
- package/dist/component/validators/daily.d.ts +17 -17
- package/dist/component/validators/plannedWorkout.d.ts +5 -5
- package/dist/component/validators/samples.d.ts +2 -2
- package/dist/component/validators/shared.d.ts +17 -17
- package/dist/component/validators/sleep.d.ts +17 -17
- package/dist/garmin/activity.d.ts +101 -0
- package/dist/garmin/activity.d.ts.map +1 -0
- package/dist/garmin/activity.js +207 -0
- package/dist/garmin/activity.js.map +1 -0
- package/dist/garmin/auth.d.ts +65 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +155 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/body.d.ts +26 -0
- package/dist/garmin/body.d.ts.map +1 -0
- package/dist/garmin/body.js +44 -0
- package/dist/garmin/body.js.map +1 -0
- package/dist/garmin/client.d.ts +99 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +153 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +74 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +143 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/index.d.ts +20 -0
- package/dist/garmin/index.d.ts.map +1 -0
- package/dist/garmin/index.js +21 -0
- package/dist/garmin/index.js.map +1 -0
- package/dist/garmin/maps/activity-type.d.ts +7 -0
- package/dist/garmin/maps/activity-type.d.ts.map +1 -0
- package/dist/garmin/maps/activity-type.js +98 -0
- package/dist/garmin/maps/activity-type.js.map +1 -0
- package/dist/garmin/maps/sleep-level.d.ts +6 -0
- package/dist/garmin/maps/sleep-level.d.ts.map +1 -0
- package/dist/garmin/maps/sleep-level.js +21 -0
- package/dist/garmin/maps/sleep-level.js.map +1 -0
- package/dist/garmin/menstruation.d.ts +23 -0
- package/dist/garmin/menstruation.d.ts.map +1 -0
- package/dist/garmin/menstruation.js +34 -0
- package/dist/garmin/menstruation.js.map +1 -0
- package/dist/garmin/sleep.d.ts +62 -0
- package/dist/garmin/sleep.d.ts.map +1 -0
- package/dist/garmin/sleep.js +125 -0
- package/dist/garmin/sleep.js.map +1 -0
- package/dist/garmin/sync.d.ts +39 -0
- package/dist/garmin/sync.d.ts.map +1 -0
- package/dist/garmin/sync.js +175 -0
- package/dist/garmin/sync.js.map +1 -0
- package/dist/garmin/types.d.ts +212 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +8 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/validators.d.ts +331 -331
- package/package.json +5 -1
- package/src/client/index.ts +194 -1
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +62 -0
- package/src/component/garmin.ts +534 -0
- package/src/component/schema.ts +3 -2
- package/src/component/strava.ts +23 -1
- package/src/garmin/activity.test.ts +178 -0
- package/src/garmin/activity.ts +272 -0
- package/src/garmin/auth.test.ts +128 -0
- package/src/garmin/auth.ts +249 -0
- package/src/garmin/body.ts +59 -0
- package/src/garmin/client.ts +254 -0
- package/src/garmin/daily.ts +211 -0
- package/src/garmin/index.ts +76 -0
- package/src/garmin/maps/activity-type.test.ts +78 -0
- package/src/garmin/maps/activity-type.ts +116 -0
- package/src/garmin/maps/sleep-level.ts +22 -0
- package/src/garmin/menstruation.ts +42 -0
- package/src/garmin/sleep.test.ts +110 -0
- package/src/garmin/sleep.ts +170 -0
- package/src/garmin/sync.ts +223 -0
- package/src/garmin/types.ts +338 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ─── Sleep Transformer ───────────────────────────────────────────────────────
|
|
2
|
+
// Transforms a Garmin sleep session into the Soma Sleep schema shape.
|
|
3
|
+
|
|
4
|
+
import type { GarminSleep, GarminSleepLevel } from "./types.js";
|
|
5
|
+
import { mapSleepLevel } from "./maps/sleep-level.js";
|
|
6
|
+
|
|
7
|
+
export type SleepData = ReturnType<typeof transformSleep>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transform a Garmin sleep session into a Soma Sleep document shape.
|
|
11
|
+
*
|
|
12
|
+
* @param sleep - The Garmin sleep data from the Health API
|
|
13
|
+
* @returns Soma Sleep fields (without connectionId/userId)
|
|
14
|
+
*/
|
|
15
|
+
export function transformSleep(sleep: GarminSleep) {
|
|
16
|
+
const startMs = sleep.startTimeInSeconds * 1000;
|
|
17
|
+
const endMs = startMs + sleep.durationInSeconds * 1000;
|
|
18
|
+
|
|
19
|
+
const uploadTypeMap: Record<string, number> = {
|
|
20
|
+
ENHANCED_FINAL: 2, // Automatic
|
|
21
|
+
ENHANCED_TENTATIVE: 4, // Indeterminate
|
|
22
|
+
AUTO_FINAL: 2, // Automatic
|
|
23
|
+
AUTO_TENTATIVE: 4, // Indeterminate
|
|
24
|
+
MANUAL: 1, // Manual
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
metadata: {
|
|
29
|
+
summary_id: sleep.summaryId,
|
|
30
|
+
start_time: new Date(startMs).toISOString(),
|
|
31
|
+
end_time: new Date(endMs).toISOString(),
|
|
32
|
+
upload_type: uploadTypeMap[sleep.validation] ?? 0,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
sleep_durations_data: buildSleepDurationsData(sleep),
|
|
36
|
+
|
|
37
|
+
heart_rate_data: buildHeartRateData(sleep),
|
|
38
|
+
|
|
39
|
+
respiration_data: buildRespirationData(sleep),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function buildSleepDurationsData(sleep: GarminSleep) {
|
|
46
|
+
const totalAsleep =
|
|
47
|
+
(sleep.deepSleepDurationInSeconds ?? 0) +
|
|
48
|
+
(sleep.lightSleepDurationInSeconds ?? 0) +
|
|
49
|
+
(sleep.remSleepInSeconds ?? 0);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
asleep: {
|
|
53
|
+
duration_asleep_state_seconds: totalAsleep || undefined,
|
|
54
|
+
duration_deep_sleep_state_seconds: sleep.deepSleepDurationInSeconds,
|
|
55
|
+
duration_light_sleep_state_seconds: sleep.lightSleepDurationInSeconds,
|
|
56
|
+
duration_REM_sleep_state_seconds: sleep.remSleepInSeconds,
|
|
57
|
+
},
|
|
58
|
+
awake: {
|
|
59
|
+
duration_awake_state_seconds: sleep.awakeDurationInSeconds,
|
|
60
|
+
},
|
|
61
|
+
other: {
|
|
62
|
+
duration_in_bed_seconds: sleep.durationInSeconds,
|
|
63
|
+
duration_unmeasurable_sleep_seconds: sleep.unmeasurableSleepInSeconds,
|
|
64
|
+
},
|
|
65
|
+
hypnogram_samples: buildHypnogramSamples(sleep),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildHypnogramSamples(sleep: GarminSleep) {
|
|
70
|
+
if (!sleep.sleepLevelsMap) return undefined;
|
|
71
|
+
|
|
72
|
+
const samples: Array<{ timestamp: string; level: number }> = [];
|
|
73
|
+
|
|
74
|
+
for (const [stage, levels] of Object.entries(sleep.sleepLevelsMap)) {
|
|
75
|
+
if (!levels) continue;
|
|
76
|
+
const terraLevel = mapSleepLevel(stage);
|
|
77
|
+
|
|
78
|
+
for (const level of levels as GarminSleepLevel[]) {
|
|
79
|
+
samples.push({
|
|
80
|
+
timestamp: new Date(level.startTimeInSeconds * 1000).toISOString(),
|
|
81
|
+
level: terraLevel,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
samples.sort(
|
|
87
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return samples.length > 0 ? samples : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildHeartRateData(sleep: GarminSleep) {
|
|
94
|
+
if (
|
|
95
|
+
!sleep.timeOffsetHeartRateSamples ||
|
|
96
|
+
Object.keys(sleep.timeOffsetHeartRateSamples).length === 0
|
|
97
|
+
) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hrSamples = Object.entries(sleep.timeOffsetHeartRateSamples).map(
|
|
102
|
+
([offset, bpm]) => ({
|
|
103
|
+
timestamp: new Date(
|
|
104
|
+
(sleep.startTimeInSeconds + parseInt(offset, 10)) * 1000,
|
|
105
|
+
).toISOString(),
|
|
106
|
+
bpm,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
detailed: { hr_samples: hrSamples },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildRespirationData(sleep: GarminSleep) {
|
|
116
|
+
const hasBreathSummary =
|
|
117
|
+
sleep.averageRespirationInBreathsPerMinute != null;
|
|
118
|
+
const hasBreathSamples =
|
|
119
|
+
sleep.timeOffsetSleepRespiration != null &&
|
|
120
|
+
Object.keys(sleep.timeOffsetSleepRespiration).length > 0;
|
|
121
|
+
const hasSpO2Samples =
|
|
122
|
+
sleep.timeOffsetSpo2Values != null &&
|
|
123
|
+
Object.keys(sleep.timeOffsetSpo2Values).length > 0;
|
|
124
|
+
|
|
125
|
+
if (!hasBreathSummary && !hasBreathSamples && !hasSpO2Samples) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const breathSamples = hasBreathSamples
|
|
130
|
+
? Object.entries(sleep.timeOffsetSleepRespiration!).map(
|
|
131
|
+
([offset, rate]) => ({
|
|
132
|
+
timestamp: new Date(
|
|
133
|
+
(sleep.startTimeInSeconds + parseInt(offset, 10)) * 1000,
|
|
134
|
+
).toISOString(),
|
|
135
|
+
breaths_per_min: rate,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
const spo2Samples = hasSpO2Samples
|
|
141
|
+
? Object.entries(sleep.timeOffsetSpo2Values!).map(([offset, pct]) => ({
|
|
142
|
+
timestamp: new Date(
|
|
143
|
+
(sleep.startTimeInSeconds + parseInt(offset, 10)) * 1000,
|
|
144
|
+
).toISOString(),
|
|
145
|
+
percentage: pct,
|
|
146
|
+
}))
|
|
147
|
+
: undefined;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
breaths_data:
|
|
151
|
+
hasBreathSummary || (breathSamples && breathSamples.length > 0)
|
|
152
|
+
? {
|
|
153
|
+
avg_breaths_per_min: sleep.averageRespirationInBreathsPerMinute,
|
|
154
|
+
min_breaths_per_min: sleep.lowestRespirationInBreathsPerMinute,
|
|
155
|
+
max_breaths_per_min: sleep.highestRespirationInBreathsPerMinute,
|
|
156
|
+
samples:
|
|
157
|
+
breathSamples && breathSamples.length > 0
|
|
158
|
+
? breathSamples
|
|
159
|
+
: undefined,
|
|
160
|
+
}
|
|
161
|
+
: undefined,
|
|
162
|
+
oxygen_saturation_data:
|
|
163
|
+
spo2Samples && spo2Samples.length > 0
|
|
164
|
+
? {
|
|
165
|
+
avg_saturation_percentage: sleep.averageSpo2Value,
|
|
166
|
+
samples: spo2Samples,
|
|
167
|
+
}
|
|
168
|
+
: undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// ─── Garmin Sync Helpers ─────────────────────────────────────────────────────
|
|
2
|
+
// High-level functions that combine the Garmin client, transformers,
|
|
3
|
+
// and Soma ingestion. Designed to be used inside a Convex action.
|
|
4
|
+
|
|
5
|
+
import type { Soma } from "../client/index.js";
|
|
6
|
+
import type { ActionCtx } from "../client/types.js";
|
|
7
|
+
import type { GarminClient } from "./client.js";
|
|
8
|
+
import type { TimeRangeParams } from "./client.js";
|
|
9
|
+
import { transformActivity } from "./activity.js";
|
|
10
|
+
import { transformDaily } from "./daily.js";
|
|
11
|
+
import { transformSleep } from "./sleep.js";
|
|
12
|
+
import { transformBody } from "./body.js";
|
|
13
|
+
import { transformMenstruation } from "./menstruation.js";
|
|
14
|
+
|
|
15
|
+
// ─── Shared Types ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface SyncOptions {
|
|
18
|
+
client: GarminClient;
|
|
19
|
+
soma: Soma;
|
|
20
|
+
ctx: ActionCtx;
|
|
21
|
+
connectionId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
timeRange: TimeRangeParams;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SyncResult {
|
|
27
|
+
synced: number;
|
|
28
|
+
errors: Array<{ id: string; error: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SyncAllResult {
|
|
32
|
+
activities: SyncResult;
|
|
33
|
+
dailies: SyncResult;
|
|
34
|
+
sleep: SyncResult;
|
|
35
|
+
body: SyncResult;
|
|
36
|
+
menstruation: SyncResult;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Sync All ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sync all data types from Garmin for a given time range.
|
|
43
|
+
*
|
|
44
|
+
* Runs each sync independently so a failure in one type doesn't
|
|
45
|
+
* block the others.
|
|
46
|
+
*/
|
|
47
|
+
export async function syncAll(opts: SyncOptions): Promise<SyncAllResult> {
|
|
48
|
+
const [activities, dailies, sleep, body, menstruation] = await Promise.all([
|
|
49
|
+
syncActivities(opts),
|
|
50
|
+
syncDailies(opts),
|
|
51
|
+
syncSleep(opts),
|
|
52
|
+
syncBody(opts),
|
|
53
|
+
syncMenstruation(opts),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
return { activities, dailies, sleep, body, menstruation };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Activities ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export async function syncActivities(opts: SyncOptions): Promise<SyncResult> {
|
|
62
|
+
const { client, soma, ctx, connectionId, userId, timeRange } = opts;
|
|
63
|
+
let synced = 0;
|
|
64
|
+
const errors: SyncResult["errors"] = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const activities = await client.getActivities(timeRange);
|
|
68
|
+
|
|
69
|
+
for (const activity of activities) {
|
|
70
|
+
try {
|
|
71
|
+
const data = transformActivity(activity);
|
|
72
|
+
await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
73
|
+
synced++;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
errors.push({
|
|
76
|
+
id: activity.summaryId ?? String(activity.activityId),
|
|
77
|
+
error: err instanceof Error ? err.message : String(err),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
errors.push({
|
|
83
|
+
id: "fetch",
|
|
84
|
+
error: err instanceof Error ? err.message : String(err),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { synced, errors };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Dailies ─────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export async function syncDailies(opts: SyncOptions): Promise<SyncResult> {
|
|
94
|
+
const { client, soma, ctx, connectionId, userId, timeRange } = opts;
|
|
95
|
+
let synced = 0;
|
|
96
|
+
const errors: SyncResult["errors"] = [];
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const dailies = await client.getDailies(timeRange);
|
|
100
|
+
|
|
101
|
+
for (const daily of dailies) {
|
|
102
|
+
try {
|
|
103
|
+
const data = transformDaily(daily);
|
|
104
|
+
await soma.ingestDaily(ctx, { connectionId, userId, ...data });
|
|
105
|
+
synced++;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
errors.push({
|
|
108
|
+
id: daily.summaryId ?? daily.calendarDate,
|
|
109
|
+
error: err instanceof Error ? err.message : String(err),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
errors.push({
|
|
115
|
+
id: "fetch",
|
|
116
|
+
error: err instanceof Error ? err.message : String(err),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { synced, errors };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Sleep ───────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export async function syncSleep(opts: SyncOptions): Promise<SyncResult> {
|
|
126
|
+
const { client, soma, ctx, connectionId, userId, timeRange } = opts;
|
|
127
|
+
let synced = 0;
|
|
128
|
+
const errors: SyncResult["errors"] = [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const sleeps = await client.getSleeps(timeRange);
|
|
132
|
+
|
|
133
|
+
for (const sleep of sleeps) {
|
|
134
|
+
try {
|
|
135
|
+
const data = transformSleep(sleep);
|
|
136
|
+
await soma.ingestSleep(ctx, { connectionId, userId, ...data });
|
|
137
|
+
synced++;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
errors.push({
|
|
140
|
+
id: sleep.summaryId ?? sleep.calendarDate,
|
|
141
|
+
error: err instanceof Error ? err.message : String(err),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
errors.push({
|
|
147
|
+
id: "fetch",
|
|
148
|
+
error: err instanceof Error ? err.message : String(err),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { synced, errors };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Body Composition ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export async function syncBody(opts: SyncOptions): Promise<SyncResult> {
|
|
158
|
+
const { client, soma, ctx, connectionId, userId, timeRange } = opts;
|
|
159
|
+
let synced = 0;
|
|
160
|
+
const errors: SyncResult["errors"] = [];
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const bodyComps = await client.getBodyCompositions(timeRange);
|
|
164
|
+
|
|
165
|
+
for (const body of bodyComps) {
|
|
166
|
+
try {
|
|
167
|
+
const data = transformBody(body);
|
|
168
|
+
await soma.ingestBody(ctx, { connectionId, userId, ...data });
|
|
169
|
+
synced++;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
errors.push({
|
|
172
|
+
id: body.summaryId ?? String(body.measurementTimeInSeconds),
|
|
173
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
errors.push({
|
|
179
|
+
id: "fetch",
|
|
180
|
+
error: err instanceof Error ? err.message : String(err),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { synced, errors };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Menstruation ────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export async function syncMenstruation(
|
|
190
|
+
opts: SyncOptions,
|
|
191
|
+
): Promise<SyncResult> {
|
|
192
|
+
const { client, soma, ctx, connectionId, userId, timeRange } = opts;
|
|
193
|
+
let synced = 0;
|
|
194
|
+
const errors: SyncResult["errors"] = [];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const records = await client.getMenstrualCycleData(timeRange);
|
|
198
|
+
|
|
199
|
+
for (const record of records) {
|
|
200
|
+
try {
|
|
201
|
+
const data = transformMenstruation(record);
|
|
202
|
+
await soma.ingestMenstruation(ctx, {
|
|
203
|
+
connectionId,
|
|
204
|
+
userId,
|
|
205
|
+
...data,
|
|
206
|
+
});
|
|
207
|
+
synced++;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
errors.push({
|
|
210
|
+
id: record.summaryId ?? record.calendarDate,
|
|
211
|
+
error: err instanceof Error ? err.message : String(err),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
errors.push({
|
|
217
|
+
id: "fetch",
|
|
218
|
+
error: err instanceof Error ? err.message : String(err),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { synced, errors };
|
|
223
|
+
}
|