@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/component/private.ts
CHANGED
|
@@ -34,6 +34,27 @@ export const getConnectionByProvider = internalQuery({
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Get a connection by provider's external user ID + provider (internal).
|
|
39
|
+
* Used by webhook handlers to map Garmin's userId to a Soma connection.
|
|
40
|
+
*/
|
|
41
|
+
export const getConnectionByProviderUserId = internalQuery({
|
|
42
|
+
args: {
|
|
43
|
+
providerUserId: v.string(),
|
|
44
|
+
provider: v.string(),
|
|
45
|
+
},
|
|
46
|
+
handler: async (ctx, args) => {
|
|
47
|
+
return await ctx.db
|
|
48
|
+
.query("connections")
|
|
49
|
+
.withIndex("by_providerUserId_provider", (q) =>
|
|
50
|
+
q
|
|
51
|
+
.eq("providerUserId", args.providerUserId)
|
|
52
|
+
.eq("provider", args.provider),
|
|
53
|
+
)
|
|
54
|
+
.first();
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
37
58
|
/**
|
|
38
59
|
* Update the lastDataUpdate timestamp after a successful sync.
|
|
39
60
|
*/
|
package/src/component/public.ts
CHANGED
|
@@ -17,6 +17,7 @@ const connectionDoc = v.object({
|
|
|
17
17
|
_creationTime: v.number(),
|
|
18
18
|
userId: v.string(),
|
|
19
19
|
provider: v.string(),
|
|
20
|
+
providerUserId: v.optional(v.string()),
|
|
20
21
|
active: v.optional(v.boolean()),
|
|
21
22
|
lastDataUpdate: v.optional(v.string()),
|
|
22
23
|
});
|
|
@@ -33,6 +34,7 @@ export const connect = mutation({
|
|
|
33
34
|
args: {
|
|
34
35
|
userId: v.string(),
|
|
35
36
|
provider: v.string(),
|
|
37
|
+
providerUserId: v.optional(v.string()),
|
|
36
38
|
},
|
|
37
39
|
returns: v.id("connections"),
|
|
38
40
|
handler: async (ctx, args) => {
|
|
@@ -44,8 +46,13 @@ export const connect = mutation({
|
|
|
44
46
|
.first();
|
|
45
47
|
|
|
46
48
|
if (existing) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const patch: Record<string, unknown> = {};
|
|
50
|
+
if (!existing.active) patch.active = true;
|
|
51
|
+
if (args.providerUserId && !existing.providerUserId) {
|
|
52
|
+
patch.providerUserId = args.providerUserId;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(patch).length > 0) {
|
|
55
|
+
await ctx.db.patch(existing._id, patch);
|
|
49
56
|
}
|
|
50
57
|
return existing._id;
|
|
51
58
|
}
|
|
@@ -53,6 +60,7 @@ export const connect = mutation({
|
|
|
53
60
|
return await ctx.db.insert("connections", {
|
|
54
61
|
userId: args.userId,
|
|
55
62
|
provider: args.provider,
|
|
63
|
+
providerUserId: args.providerUserId,
|
|
56
64
|
active: true,
|
|
57
65
|
});
|
|
58
66
|
},
|
|
@@ -142,6 +150,7 @@ export const listConnections = query({
|
|
|
142
150
|
export const updateConnection = mutation({
|
|
143
151
|
args: {
|
|
144
152
|
connectionId: v.id("connections"),
|
|
153
|
+
providerUserId: v.optional(v.string()),
|
|
145
154
|
active: v.optional(v.boolean()),
|
|
146
155
|
lastDataUpdate: v.optional(v.string()),
|
|
147
156
|
},
|
package/src/component/schema.ts
CHANGED
|
@@ -29,7 +29,8 @@ export default defineSchema({
|
|
|
29
29
|
connections: defineTable(connectionValidator)
|
|
30
30
|
.index("by_userId", ["userId"])
|
|
31
31
|
.index("by_provider", ["provider"])
|
|
32
|
-
.index("by_userId_provider", ["userId", "provider"])
|
|
32
|
+
.index("by_userId_provider", ["userId", "provider"])
|
|
33
|
+
.index("by_providerUserId_provider", ["providerUserId", "provider"]),
|
|
33
34
|
|
|
34
35
|
// ── Athletes ───────────────────────────────────────────────────────────────
|
|
35
36
|
// User profile/identifying information from the provider.
|
|
@@ -9,6 +9,8 @@ export const connectionValidator = {
|
|
|
9
9
|
userId: v.string(),
|
|
10
10
|
// The wearable provider: "FITBIT", "GARMIN", "APPLE", "OURA", etc.
|
|
11
11
|
provider: v.string(),
|
|
12
|
+
// The provider's external user ID (e.g. Garmin's userId for webhook mapping)
|
|
13
|
+
providerUserId: v.optional(v.string()),
|
|
12
14
|
// Whether the connection is active
|
|
13
15
|
active: v.optional(v.boolean()),
|
|
14
16
|
// ISO-8601 timestamp of last data update
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import {
|
|
3
3
|
activityLevelSample,
|
|
4
|
+
breathSample,
|
|
4
5
|
calorieSample,
|
|
5
6
|
distanceSample,
|
|
6
7
|
elevationSample,
|
|
@@ -120,6 +121,20 @@ export const dailyValidator = {
|
|
|
120
121
|
upload_type: v.number(), // UploadType enum
|
|
121
122
|
}),
|
|
122
123
|
|
|
124
|
+
// ── respiration_data ────────────────────────────────────────────────────
|
|
125
|
+
respiration_data: v.optional(
|
|
126
|
+
v.object({
|
|
127
|
+
breaths_data: v.optional(
|
|
128
|
+
v.object({
|
|
129
|
+
avg_breaths_per_min: v.optional(v.number()),
|
|
130
|
+
max_breaths_per_min: v.optional(v.number()),
|
|
131
|
+
min_breaths_per_min: v.optional(v.number()),
|
|
132
|
+
samples: v.optional(v.array(breathSample)),
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
|
|
123
138
|
// ── oxygen_data ──────────────────────────────────────────────────────────
|
|
124
139
|
oxygen_data: v.optional(
|
|
125
140
|
v.object({
|
|
@@ -106,5 +106,9 @@ export const plannedWorkoutValidator = {
|
|
|
106
106
|
pool_length_meters: v.optional(v.number()),
|
|
107
107
|
estimated_calories: v.optional(v.number()),
|
|
108
108
|
estimated_duration_seconds: v.optional(v.number()),
|
|
109
|
+
// Provider-assigned workout ID (e.g. Garmin workoutId after push)
|
|
110
|
+
provider_workout_id: v.optional(v.string()),
|
|
111
|
+
// Provider-assigned schedule ID (e.g. Garmin scheduleId after push)
|
|
112
|
+
provider_schedule_id: v.optional(v.string()),
|
|
109
113
|
}),
|
|
110
114
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { transformActivity } from "./activity.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GarminActivityExtended } from "./types.js";
|
|
4
4
|
|
|
5
|
-
const baseActivity:
|
|
5
|
+
const baseActivity: GarminActivityExtended = {
|
|
6
6
|
userId: "garmin_user_1",
|
|
7
|
-
userAccessToken: "token",
|
|
8
7
|
summaryId: "summary_12345",
|
|
9
8
|
activityId: 12345,
|
|
10
9
|
activityName: "Morning Run",
|
|
@@ -38,7 +37,7 @@ describe("transformActivity", () => {
|
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
it("maps calories data", () => {
|
|
41
|
-
const withCalories:
|
|
40
|
+
const withCalories: GarminActivityExtended = {
|
|
42
41
|
...baseActivity,
|
|
43
42
|
activeKilocalories: 450,
|
|
44
43
|
bmrKilocalories: 75,
|
|
@@ -57,7 +56,7 @@ describe("transformActivity", () => {
|
|
|
57
56
|
});
|
|
58
57
|
|
|
59
58
|
it("maps distance data", () => {
|
|
60
|
-
const withDistance:
|
|
59
|
+
const withDistance: GarminActivityExtended = {
|
|
61
60
|
...baseActivity,
|
|
62
61
|
distanceInMeters: 10000,
|
|
63
62
|
elevationGainInMeters: 150,
|
|
@@ -73,7 +72,7 @@ describe("transformActivity", () => {
|
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
it("maps heart rate summary", () => {
|
|
76
|
-
const withHR:
|
|
75
|
+
const withHR: GarminActivityExtended = {
|
|
77
76
|
...baseActivity,
|
|
78
77
|
averageHeartRateInBeatsPerMinute: 155,
|
|
79
78
|
maxHeartRateInBeatsPerMinute: 185,
|
|
@@ -86,7 +85,7 @@ describe("transformActivity", () => {
|
|
|
86
85
|
});
|
|
87
86
|
|
|
88
87
|
it("maps movement data with speed and cadence", () => {
|
|
89
|
-
const withMovement:
|
|
88
|
+
const withMovement: GarminActivityExtended = {
|
|
90
89
|
...baseActivity,
|
|
91
90
|
averageSpeedInMetersPerSecond: 3.5,
|
|
92
91
|
maxSpeedInMetersPerSecond: 5.2,
|
|
@@ -103,7 +102,7 @@ describe("transformActivity", () => {
|
|
|
103
102
|
});
|
|
104
103
|
|
|
105
104
|
it("maps device data", () => {
|
|
106
|
-
const withDevice:
|
|
105
|
+
const withDevice: GarminActivityExtended = {
|
|
107
106
|
...baseActivity,
|
|
108
107
|
deviceName: "Garmin Forerunner 265",
|
|
109
108
|
};
|
|
@@ -114,7 +113,7 @@ describe("transformActivity", () => {
|
|
|
114
113
|
});
|
|
115
114
|
|
|
116
115
|
it("maps position data", () => {
|
|
117
|
-
const withPosition:
|
|
116
|
+
const withPosition: GarminActivityExtended = {
|
|
118
117
|
...baseActivity,
|
|
119
118
|
startingLatitudeInDegree: 37.7749,
|
|
120
119
|
startingLongitudeInDegree: -122.4194,
|
|
@@ -126,7 +125,7 @@ describe("transformActivity", () => {
|
|
|
126
125
|
});
|
|
127
126
|
|
|
128
127
|
it("maps power data", () => {
|
|
129
|
-
const withPower:
|
|
128
|
+
const withPower: GarminActivityExtended = {
|
|
130
129
|
...baseActivity,
|
|
131
130
|
activityType: "CYCLING",
|
|
132
131
|
averagePowerInWatts: 200,
|
|
@@ -140,22 +139,14 @@ describe("transformActivity", () => {
|
|
|
140
139
|
});
|
|
141
140
|
|
|
142
141
|
it("maps lap data", () => {
|
|
143
|
-
const withLaps:
|
|
142
|
+
const withLaps: GarminActivityExtended = {
|
|
144
143
|
...baseActivity,
|
|
145
144
|
laps: [
|
|
146
145
|
{
|
|
147
146
|
startTimeInSeconds: 1700000000,
|
|
148
|
-
timerDurationInSeconds: 600,
|
|
149
|
-
totalDistanceInMeters: 1600,
|
|
150
|
-
heartRate: 150,
|
|
151
|
-
maxSpeed: 4.5,
|
|
152
147
|
},
|
|
153
148
|
{
|
|
154
149
|
startTimeInSeconds: 1700000600,
|
|
155
|
-
timerDurationInSeconds: 600,
|
|
156
|
-
totalDistanceInMeters: 1650,
|
|
157
|
-
heartRate: 158,
|
|
158
|
-
maxSpeed: 4.8,
|
|
159
150
|
},
|
|
160
151
|
],
|
|
161
152
|
};
|
|
@@ -163,8 +154,9 @@ describe("transformActivity", () => {
|
|
|
163
154
|
|
|
164
155
|
expect(result.lap_data).toBeDefined();
|
|
165
156
|
expect(result.lap_data!.laps).toHaveLength(2);
|
|
166
|
-
expect(result.lap_data!.laps![0].
|
|
167
|
-
|
|
157
|
+
expect(result.lap_data!.laps![0].start_time).toBe(
|
|
158
|
+
new Date(1700000000 * 1000).toISOString(),
|
|
159
|
+
);
|
|
168
160
|
});
|
|
169
161
|
|
|
170
162
|
it("uses summaryId as summary_id and falls back to activityId", () => {
|
package/src/garmin/activity.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── Activity Transformer ────────────────────────────────────────────────────
|
|
2
2
|
// Transforms a Garmin activity into the Soma Activity schema shape.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { GarminActivityExtended, GarminLap, GarminSample } from "./types.js";
|
|
5
5
|
import { mapActivityType } from "./maps/activity-type.js";
|
|
6
6
|
|
|
7
7
|
export type ActivityData = ReturnType<typeof transformActivity>;
|
|
@@ -9,21 +9,16 @@ export type ActivityData = ReturnType<typeof transformActivity>;
|
|
|
9
9
|
/**
|
|
10
10
|
* Transform a Garmin activity into a Soma Activity document shape.
|
|
11
11
|
*
|
|
12
|
+
* Accepts both activity summaries (from `/rest/activities`) and detailed
|
|
13
|
+
* activities (from `/rest/activityDetails` or webhook payloads) which
|
|
14
|
+
* include laps, samples, and power data.
|
|
15
|
+
*
|
|
12
16
|
* The returned object is ready to be spread into an `ingestActivity` call
|
|
13
17
|
* alongside `connectionId` and `userId`.
|
|
14
|
-
*
|
|
15
|
-
* @param activity - The Garmin activity from the Health API
|
|
16
|
-
* @returns Soma Activity fields (without connectionId/userId)
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```ts
|
|
20
|
-
* const data = transformActivity(garminActivity);
|
|
21
|
-
* await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
22
|
-
* ```
|
|
23
18
|
*/
|
|
24
|
-
export function transformActivity(activity:
|
|
25
|
-
const startMs = activity.startTimeInSeconds * 1000;
|
|
26
|
-
const endMs = startMs + activity.durationInSeconds * 1000;
|
|
19
|
+
export function transformActivity(activity: GarminActivityExtended) {
|
|
20
|
+
const startMs = (activity.startTimeInSeconds ?? 0) * 1000;
|
|
21
|
+
const endMs = startMs + (activity.durationInSeconds ?? 0) * 1000;
|
|
27
22
|
const startDate = new Date(startMs).toISOString();
|
|
28
23
|
const endDate = new Date(endMs).toISOString();
|
|
29
24
|
|
|
@@ -32,7 +27,7 @@ export function transformActivity(activity: GarminActivity) {
|
|
|
32
27
|
summary_id: activity.summaryId ?? String(activity.activityId),
|
|
33
28
|
start_time: startDate,
|
|
34
29
|
end_time: endDate,
|
|
35
|
-
type: mapActivityType(activity.activityType),
|
|
30
|
+
type: mapActivityType(activity.activityType ?? "OTHER"),
|
|
36
31
|
upload_type: activity.manual ? 2 : 1,
|
|
37
32
|
name: activity.activityName,
|
|
38
33
|
},
|
|
@@ -63,7 +58,7 @@ export function transformActivity(activity: GarminActivity) {
|
|
|
63
58
|
|
|
64
59
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
65
60
|
|
|
66
|
-
function buildCaloriesData(activity:
|
|
61
|
+
function buildCaloriesData(activity: GarminActivityExtended) {
|
|
67
62
|
if (
|
|
68
63
|
activity.activeKilocalories == null &&
|
|
69
64
|
activity.bmrKilocalories == null
|
|
@@ -81,8 +76,12 @@ function buildCaloriesData(activity: GarminActivity) {
|
|
|
81
76
|
};
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
function buildDistanceData(activity:
|
|
85
|
-
|
|
79
|
+
function buildDistanceData(activity: GarminActivityExtended) {
|
|
80
|
+
// Spec uses totalElevationGainInMeters; webhooks may use elevationGainInMeters
|
|
81
|
+
const elevGain = activity.totalElevationGainInMeters ?? activity.elevationGainInMeters;
|
|
82
|
+
const elevLoss = activity.totalElevationLossInMeters ?? activity.elevationLossInMeters;
|
|
83
|
+
|
|
84
|
+
if (activity.distanceInMeters == null && elevGain == null) {
|
|
86
85
|
return undefined;
|
|
87
86
|
}
|
|
88
87
|
|
|
@@ -91,17 +90,17 @@ function buildDistanceData(activity: GarminActivity) {
|
|
|
91
90
|
distance_meters: activity.distanceInMeters,
|
|
92
91
|
steps: activity.steps,
|
|
93
92
|
elevation:
|
|
94
|
-
|
|
93
|
+
elevGain != null
|
|
95
94
|
? {
|
|
96
|
-
gain_actual_meters:
|
|
97
|
-
loss_actual_meters:
|
|
95
|
+
gain_actual_meters: elevGain,
|
|
96
|
+
loss_actual_meters: elevLoss,
|
|
98
97
|
}
|
|
99
98
|
: undefined,
|
|
100
99
|
},
|
|
101
100
|
};
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
function buildHeartRateData(activity:
|
|
103
|
+
function buildHeartRateData(activity: GarminActivityExtended) {
|
|
105
104
|
const hasHrSummary =
|
|
106
105
|
activity.averageHeartRateInBeatsPerMinute != null ||
|
|
107
106
|
activity.maxHeartRateInBeatsPerMinute != null;
|
|
@@ -111,8 +110,8 @@ function buildHeartRateData(activity: GarminActivity) {
|
|
|
111
110
|
|
|
112
111
|
const hrSamples = hasSamples
|
|
113
112
|
? activity.samples!
|
|
114
|
-
.filter((s) => s.heartRate != null && s.startTimeInSeconds != null)
|
|
115
|
-
.map((s) => ({
|
|
113
|
+
.filter((s: GarminSample) => s.heartRate != null && s.startTimeInSeconds != null)
|
|
114
|
+
.map((s: GarminSample) => ({
|
|
116
115
|
timestamp: new Date(s.startTimeInSeconds! * 1000).toISOString(),
|
|
117
116
|
bpm: s.heartRate!,
|
|
118
117
|
}))
|
|
@@ -132,7 +131,7 @@ function buildHeartRateData(activity: GarminActivity) {
|
|
|
132
131
|
};
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
function buildMovementData(activity:
|
|
134
|
+
function buildMovementData(activity: GarminActivityExtended) {
|
|
136
135
|
const avgCadence =
|
|
137
136
|
activity.averageRunCadenceInStepsPerMinute ??
|
|
138
137
|
activity.averageBikeCadenceInRoundsPerMinute;
|
|
@@ -152,10 +151,10 @@ function buildMovementData(activity: GarminActivity) {
|
|
|
152
151
|
const speedSamples = hasSamples
|
|
153
152
|
? activity.samples!
|
|
154
153
|
.filter(
|
|
155
|
-
(s) =>
|
|
154
|
+
(s: GarminSample) =>
|
|
156
155
|
s.speedMetersPerSecond != null && s.startTimeInSeconds != null,
|
|
157
156
|
)
|
|
158
|
-
.map((s) => ({
|
|
157
|
+
.map((s: GarminSample) => ({
|
|
159
158
|
timestamp: new Date(s.startTimeInSeconds! * 1000).toISOString(),
|
|
160
159
|
speed_meters_per_second: s.speedMetersPerSecond!,
|
|
161
160
|
}))
|
|
@@ -164,15 +163,15 @@ function buildMovementData(activity: GarminActivity) {
|
|
|
164
163
|
const cadenceSamples = hasSamples
|
|
165
164
|
? activity.samples!
|
|
166
165
|
.filter(
|
|
167
|
-
(s) =>
|
|
168
|
-
(s.
|
|
166
|
+
(s: GarminSample) =>
|
|
167
|
+
(s.stepsPerMinute != null ||
|
|
169
168
|
s.bikeCadenceInRPM != null) &&
|
|
170
169
|
s.startTimeInSeconds != null,
|
|
171
170
|
)
|
|
172
|
-
.map((s) => ({
|
|
171
|
+
.map((s: GarminSample) => ({
|
|
173
172
|
timestamp: new Date(s.startTimeInSeconds! * 1000).toISOString(),
|
|
174
173
|
cadence_rpm:
|
|
175
|
-
s.
|
|
174
|
+
s.stepsPerMinute ?? s.bikeCadenceInRPM ?? 0,
|
|
176
175
|
}))
|
|
177
176
|
: undefined;
|
|
178
177
|
|
|
@@ -192,7 +191,7 @@ function buildMovementData(activity: GarminActivity) {
|
|
|
192
191
|
};
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
function buildPositionData(activity:
|
|
194
|
+
function buildPositionData(activity: GarminActivityExtended) {
|
|
196
195
|
const hasStartPos =
|
|
197
196
|
activity.startingLatitudeInDegree != null &&
|
|
198
197
|
activity.startingLongitudeInDegree != null;
|
|
@@ -203,12 +202,12 @@ function buildPositionData(activity: GarminActivity) {
|
|
|
203
202
|
const positionSamples = hasSamples
|
|
204
203
|
? activity.samples!
|
|
205
204
|
.filter(
|
|
206
|
-
(s) =>
|
|
205
|
+
(s: GarminSample) =>
|
|
207
206
|
s.latitudeInDegree != null &&
|
|
208
207
|
s.longitudeInDegree != null &&
|
|
209
208
|
s.startTimeInSeconds != null,
|
|
210
209
|
)
|
|
211
|
-
.map((s) => ({
|
|
210
|
+
.map((s: GarminSample) => ({
|
|
212
211
|
timestamp: new Date(s.startTimeInSeconds! * 1000).toISOString(),
|
|
213
212
|
coords_lat_lng_deg: [s.latitudeInDegree!, s.longitudeInDegree!],
|
|
214
213
|
}))
|
|
@@ -228,7 +227,7 @@ function buildPositionData(activity: GarminActivity) {
|
|
|
228
227
|
};
|
|
229
228
|
}
|
|
230
229
|
|
|
231
|
-
function buildPowerData(activity:
|
|
230
|
+
function buildPowerData(activity: GarminActivityExtended) {
|
|
232
231
|
const hasPowerSummary =
|
|
233
232
|
activity.averagePowerInWatts != null ||
|
|
234
233
|
activity.maxPowerInWatts != null;
|
|
@@ -239,9 +238,9 @@ function buildPowerData(activity: GarminActivity) {
|
|
|
239
238
|
const powerSamples = hasSamples
|
|
240
239
|
? activity.samples!
|
|
241
240
|
.filter(
|
|
242
|
-
(s) => s.powerInWatts != null && s.startTimeInSeconds != null,
|
|
241
|
+
(s: GarminSample) => s.powerInWatts != null && s.startTimeInSeconds != null,
|
|
243
242
|
)
|
|
244
|
-
.map((s) => ({
|
|
243
|
+
.map((s: GarminSample) => ({
|
|
245
244
|
timestamp: new Date(s.startTimeInSeconds! * 1000).toISOString(),
|
|
246
245
|
watts: s.powerInWatts!,
|
|
247
246
|
}))
|
|
@@ -255,18 +254,12 @@ function buildPowerData(activity: GarminActivity) {
|
|
|
255
254
|
};
|
|
256
255
|
}
|
|
257
256
|
|
|
258
|
-
function buildLapData(activity:
|
|
257
|
+
function buildLapData(activity: GarminActivityExtended) {
|
|
259
258
|
if (!activity.laps || activity.laps.length === 0) return undefined;
|
|
260
259
|
|
|
261
260
|
return {
|
|
262
|
-
laps: activity.laps.map((lap) => ({
|
|
263
|
-
start_time: new Date(lap.startTimeInSeconds * 1000).toISOString(),
|
|
264
|
-
end_time: new Date(
|
|
265
|
-
(lap.startTimeInSeconds + lap.timerDurationInSeconds) * 1000,
|
|
266
|
-
).toISOString(),
|
|
267
|
-
distance_meters: lap.totalDistanceInMeters,
|
|
268
|
-
avg_hr_bpm: lap.heartRate,
|
|
269
|
-
avg_speed_meters_per_second: lap.maxSpeed,
|
|
261
|
+
laps: activity.laps.map((lap: GarminLap) => ({
|
|
262
|
+
start_time: new Date((lap.startTimeInSeconds ?? 0) * 1000).toISOString(),
|
|
270
263
|
})),
|
|
271
264
|
};
|
|
272
265
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── Blood Pressure Transformer ──────────────────────────────────────────────
|
|
2
|
+
// Transforms Garmin blood pressure data into the Soma Body schema shape.
|
|
3
|
+
|
|
4
|
+
import type { GarminBloodPressure } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type BloodPressureData = ReturnType<typeof transformBloodPressure>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Transform a Garmin blood pressure record into a Soma Body document shape.
|
|
10
|
+
*
|
|
11
|
+
* @param bp - The Garmin blood pressure data from the Health API
|
|
12
|
+
* @returns Soma Body fields (without connectionId/userId)
|
|
13
|
+
*/
|
|
14
|
+
export function transformBloodPressure(bp: GarminBloodPressure) {
|
|
15
|
+
const measurementMs = (bp.measurementTimeInSeconds ?? 0) * 1000;
|
|
16
|
+
const timestamp = new Date(measurementMs).toISOString();
|
|
17
|
+
|
|
18
|
+
if (bp.systolic == null && bp.diastolic == null) {
|
|
19
|
+
return {
|
|
20
|
+
metadata: { start_time: timestamp, end_time: timestamp },
|
|
21
|
+
blood_pressure_data: undefined,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
metadata: {
|
|
27
|
+
start_time: timestamp,
|
|
28
|
+
end_time: timestamp,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
blood_pressure_data: {
|
|
32
|
+
blood_pressure_samples: [
|
|
33
|
+
{
|
|
34
|
+
timestamp,
|
|
35
|
+
diastolic_bp: bp.diastolic,
|
|
36
|
+
systolic_bp: bp.systolic,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/garmin/body.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type BodyData = ReturnType<typeof transformBody>;
|
|
|
12
12
|
* @returns Soma Body fields (without connectionId/userId)
|
|
13
13
|
*/
|
|
14
14
|
export function transformBody(body: GarminBodyComposition) {
|
|
15
|
-
const measurementMs = body.measurementTimeInSeconds * 1000;
|
|
15
|
+
const measurementMs = (body.measurementTimeInSeconds ?? 0) * 1000;
|
|
16
16
|
const timestamp = new Date(measurementMs).toISOString();
|
|
17
17
|
|
|
18
18
|
return {
|