@cmichel/healthlog 0.1.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.
Files changed (64) hide show
  1. package/README.md +40 -0
  2. package/dist/cli.js +49 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/dump.js +26 -0
  5. package/dist/commands/dump.js.map +1 -0
  6. package/dist/commands/setup-garmin.js +30 -0
  7. package/dist/commands/setup-garmin.js.map +1 -0
  8. package/dist/commands/setup-hevy.js +27 -0
  9. package/dist/commands/setup-hevy.js.map +1 -0
  10. package/dist/config/database-path.js +34 -0
  11. package/dist/config/database-path.js.map +1 -0
  12. package/dist/db/database.js +20 -0
  13. package/dist/db/database.js.map +1 -0
  14. package/dist/db/endurance-metrics.js +43 -0
  15. package/dist/db/endurance-metrics.js.map +1 -0
  16. package/dist/db/provider-state.js +48 -0
  17. package/dist/db/provider-state.js.map +1 -0
  18. package/dist/db/schema.js +53 -0
  19. package/dist/db/schema.js.map +1 -0
  20. package/dist/db/strength-metrics.js +14 -0
  21. package/dist/db/strength-metrics.js.map +1 -0
  22. package/dist/db/workouts.js +172 -0
  23. package/dist/db/workouts.js.map +1 -0
  24. package/dist/domain/dump.js +2 -0
  25. package/dist/domain/dump.js.map +1 -0
  26. package/dist/domain/provider.js +2 -0
  27. package/dist/domain/provider.js.map +1 -0
  28. package/dist/domain/workout.js +23 -0
  29. package/dist/domain/workout.js.map +1 -0
  30. package/dist/providers/garmin/client.js +38 -0
  31. package/dist/providers/garmin/client.js.map +1 -0
  32. package/dist/providers/garmin/normalize.js +145 -0
  33. package/dist/providers/garmin/normalize.js.map +1 -0
  34. package/dist/providers/garmin/source.js +49 -0
  35. package/dist/providers/garmin/source.js.map +1 -0
  36. package/dist/providers/garmin/sync.js +72 -0
  37. package/dist/providers/garmin/sync.js.map +1 -0
  38. package/dist/providers/garmin/types.js +129 -0
  39. package/dist/providers/garmin/types.js.map +1 -0
  40. package/dist/providers/hevy/client.js +42 -0
  41. package/dist/providers/hevy/client.js.map +1 -0
  42. package/dist/providers/hevy/normalize.js +49 -0
  43. package/dist/providers/hevy/normalize.js.map +1 -0
  44. package/dist/providers/hevy/source.js +6 -0
  45. package/dist/providers/hevy/source.js.map +1 -0
  46. package/dist/providers/hevy/sync.js +88 -0
  47. package/dist/providers/hevy/sync.js.map +1 -0
  48. package/dist/providers/hevy/types.js +73 -0
  49. package/dist/providers/hevy/types.js.map +1 -0
  50. package/dist/services/dump-service.js +71 -0
  51. package/dist/services/dump-service.js.map +1 -0
  52. package/dist/services/setup-service.js +11 -0
  53. package/dist/services/setup-service.js.map +1 -0
  54. package/dist/services/sync-service.js +32 -0
  55. package/dist/services/sync-service.js.map +1 -0
  56. package/dist/utils/dates.js +35 -0
  57. package/dist/utils/dates.js.map +1 -0
  58. package/dist/utils/logger.js +28 -0
  59. package/dist/utils/logger.js.map +1 -0
  60. package/dist/utils/parse.js +38 -0
  61. package/dist/utils/parse.js.map +1 -0
  62. package/dist/utils/running.js +16 -0
  63. package/dist/utils/running.js.map +1 -0
  64. package/package.json +45 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workouts.js","sourceRoot":"","sources":["../../src/db/workouts.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n EnduranceMetricsRow,\n StrengthMetricsRow,\n Workout,\n WorkoutWithMetrics,\n} from \"../domain/workout.js\";\nimport { StartLocationSchema } from \"../domain/workout.js\";\nimport { parseJson, parseSchema } from \"../utils/parse.js\";\nimport type { HealthlogDatabase } from \"./database.js\";\nimport { insertEnduranceMetrics } from \"./endurance-metrics.js\";\nimport { insertStrengthMetrics } from \"./strength-metrics.js\";\n\ntype WorkoutWithMetricsDbRow = {\n id: string;\n provider: Workout[\"provider\"];\n provider_id: string;\n type: Workout[\"type\"];\n sport: string;\n title: string;\n started_at_ms: number;\n ended_at_ms: number | null;\n source_json: string;\n provider_extras_json: string | null;\n em_workout_id: string | null;\n em_duration_seconds: number | null;\n em_distance_meters: number | null;\n em_elevation_gain_meters: number | null;\n em_elevation_loss_meters: number | null;\n em_start_location_json: string | null;\n em_calories: number | null;\n em_avg_hr: number | null;\n em_max_hr: number | null;\n em_avg_running_cadence_spm: number | null;\n em_avg_stride_length_cm: number | null;\n em_avg_pace_min_per_km: string | null;\n em_fastest_pace_min_per_km: string | null;\n em_activity_metrics_json: string | null;\n sm_workout_id: string | null;\n sm_exercises_json: string | null;\n};\n\nexport function workoutExists(db: HealthlogDatabase, id: string): boolean {\n const row = db.prepare(\"SELECT 1 FROM workouts WHERE id = ?\").get(id) as\n | { \"1\": number }\n | undefined;\n return Boolean(row);\n}\n\nexport function upsertNormalizedWorkout(\n db: HealthlogDatabase,\n workoutWithMetrics: WorkoutWithMetrics,\n): boolean {\n const inserted = !workoutExists(db, workoutWithMetrics.workout.id);\n\n db.prepare(`\n INSERT INTO workouts (\n id,\n provider,\n provider_id,\n type,\n sport,\n title,\n started_at_ms,\n ended_at_ms,\n source_json,\n provider_extras_json\n )\n VALUES (\n @id,\n @provider,\n @providerId,\n @type,\n @sport,\n @title,\n @startedAtMs,\n @endedAtMs,\n @sourceJson,\n @providerExtrasJson\n )\n ON CONFLICT(id) DO UPDATE SET\n provider = excluded.provider,\n provider_id = excluded.provider_id,\n type = excluded.type,\n sport = excluded.sport,\n title = excluded.title,\n started_at_ms = excluded.started_at_ms,\n ended_at_ms = excluded.ended_at_ms,\n source_json = excluded.source_json,\n provider_extras_json = excluded.provider_extras_json\n `).run(workoutWithMetrics.workout);\n\n db.prepare(\"DELETE FROM endurance_metrics WHERE workout_id = ?\").run(\n workoutWithMetrics.workout.id,\n );\n db.prepare(\"DELETE FROM strength_metrics WHERE workout_id = ?\").run(\n workoutWithMetrics.workout.id,\n );\n\n if (workoutWithMetrics.type === \"endurance\") {\n insertEnduranceMetrics(db, workoutWithMetrics.enduranceMetrics);\n } else {\n insertStrengthMetrics(db, workoutWithMetrics.strengthMetrics);\n }\n\n return inserted;\n}\n\nexport function getWorkoutsWithMetrics(\n db: HealthlogDatabase,\n range: { startedAtFromMs: number | null; startedAtBeforeMs: number | null },\n): WorkoutWithMetrics[] {\n const conditions: string[] = [];\n const params: Record<string, number> = {};\n\n if (range.startedAtFromMs !== null) {\n conditions.push(\"w.started_at_ms >= @startedAtFromMs\");\n params.startedAtFromMs = range.startedAtFromMs;\n }\n if (range.startedAtBeforeMs !== null) {\n conditions.push(\"w.started_at_ms < @startedAtBeforeMs\");\n params.startedAtBeforeMs = range.startedAtBeforeMs;\n }\n\n const whereClause =\n conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const rows = db\n .prepare(`\n SELECT\n w.id,\n w.provider,\n w.provider_id,\n w.type,\n w.sport,\n w.title,\n w.started_at_ms,\n w.ended_at_ms,\n w.source_json,\n w.provider_extras_json,\n em.workout_id AS em_workout_id,\n em.duration_seconds AS em_duration_seconds,\n em.distance_meters AS em_distance_meters,\n em.elevation_gain_meters AS em_elevation_gain_meters,\n em.elevation_loss_meters AS em_elevation_loss_meters,\n em.start_location_json AS em_start_location_json,\n em.calories AS em_calories,\n em.avg_hr AS em_avg_hr,\n em.max_hr AS em_max_hr,\n em.avg_running_cadence_spm AS em_avg_running_cadence_spm,\n em.avg_stride_length_cm AS em_avg_stride_length_cm,\n em.avg_pace_min_per_km AS em_avg_pace_min_per_km,\n em.fastest_pace_min_per_km AS em_fastest_pace_min_per_km,\n em.activity_metrics_json AS em_activity_metrics_json,\n sm.workout_id AS sm_workout_id,\n sm.exercises_json AS sm_exercises_json\n FROM workouts w\n LEFT JOIN endurance_metrics em ON em.workout_id = w.id\n LEFT JOIN strength_metrics sm ON sm.workout_id = w.id\n ${whereClause}\n ORDER BY w.started_at_ms ASC, w.id ASC\n `)\n .all(params) as WorkoutWithMetricsDbRow[];\n\n return rows.map(mapWorkoutWithMetrics);\n}\n\nfunction mapWorkoutWithMetrics(\n row: WorkoutWithMetricsDbRow,\n): WorkoutWithMetrics {\n if (row.type === \"endurance\") {\n return {\n type: \"endurance\",\n workout: { ...mapWorkout(row), type: \"endurance\" },\n enduranceMetrics: mapEnduranceMetrics(row),\n };\n }\n\n return {\n type: \"strength\",\n workout: { ...mapWorkout(row), type: \"strength\" },\n strengthMetrics: mapStrengthMetrics(row),\n };\n}\n\nfunction mapWorkout(row: WorkoutWithMetricsDbRow): Workout {\n return {\n id: row.id,\n provider: row.provider,\n providerId: row.provider_id,\n type: row.type,\n sport: row.sport,\n title: row.title,\n startedAtMs: row.started_at_ms,\n endedAtMs: row.ended_at_ms,\n sourceJson: row.source_json,\n providerExtrasJson: row.provider_extras_json,\n };\n}\n\nfunction mapEnduranceMetrics(\n row: WorkoutWithMetricsDbRow,\n): EnduranceMetricsRow {\n const workoutId = requireJoinedValue(row.em_workout_id, \"em.workout_id\");\n\n return {\n workoutId,\n durationSeconds: requireJoinedValue(\n row.em_duration_seconds,\n \"em.duration_seconds\",\n ),\n distanceMeters: requireJoinedValue(\n row.em_distance_meters,\n \"em.distance_meters\",\n ),\n elevationGainMeters: requireJoinedValue(\n row.em_elevation_gain_meters,\n \"em.elevation_gain_meters\",\n ),\n elevationLossMeters: requireJoinedValue(\n row.em_elevation_loss_meters,\n \"em.elevation_loss_meters\",\n ),\n startLocation: parseStartLocation(row.em_start_location_json, workoutId),\n calories: requireJoinedValue(row.em_calories, \"em.calories\"),\n averageHeartRate: requireJoinedValue(row.em_avg_hr, \"em.avg_hr\"),\n maxHeartRate: requireJoinedValue(row.em_max_hr, \"em.max_hr\"),\n averageRunningCadenceStepsPerMinute: requireJoinedValue(\n row.em_avg_running_cadence_spm,\n \"em.avg_running_cadence_spm\",\n ),\n averageStrideLengthCentimeters: requireJoinedValue(\n row.em_avg_stride_length_cm,\n \"em.avg_stride_length_cm\",\n ),\n averagePaceMinutesPerKilometer: requireJoinedValue(\n row.em_avg_pace_min_per_km,\n \"em.avg_pace_min_per_km\",\n ),\n fastestPaceMinutesPerKilometer: requireJoinedValue(\n row.em_fastest_pace_min_per_km,\n \"em.fastest_pace_min_per_km\",\n ),\n activityMetricsJson: requireJoinedValue(\n row.em_activity_metrics_json,\n \"em.activity_metrics_json\",\n ),\n };\n}\n\nfunction parseStartLocation(\n startLocationJson: string | null,\n workoutId: string,\n): EnduranceMetricsRow[\"startLocation\"] {\n if (startLocationJson === null) {\n return null;\n }\n\n return parseSchema(\n StartLocationSchema,\n parseJson(startLocationJson, `start_location_json for ${workoutId}`),\n `start location for workout ${workoutId}`,\n );\n}\n\nfunction mapStrengthMetrics(row: WorkoutWithMetricsDbRow): StrengthMetricsRow {\n return {\n workoutId: requireJoinedValue(row.sm_workout_id, \"sm.workout_id\"),\n exercisesJson: requireJoinedValue(\n row.sm_exercises_json,\n \"sm.exercises_json\",\n ),\n };\n}\n\nfunction requireJoinedValue<T>(value: T | null, column: string): T {\n if (value === null) {\n throw new Error(`Missing joined column ${column}`);\n }\n return value;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dump.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dump.js","sourceRoot":"","sources":["../../src/domain/dump.ts"],"names":[],"mappings":"","sourcesContent":["import type { Provider } from \"./provider.js\";\nimport type {\n ActivityMetric,\n StartLocation,\n StrengthExercise,\n} from \"./workout.js\";\n\nexport type DumpRange = {\n from: string | null;\n to: string | null;\n};\n\nexport type DumpDocument = {\n generatedAt: string;\n range: DumpRange;\n workouts: DumpWorkout[];\n};\n\nexport type DumpWorkout = DumpEnduranceWorkout | DumpStrengthWorkout;\n\ntype DumpWorkoutBase = {\n id: string;\n provider: Provider;\n providerId: string;\n type: \"endurance\" | \"strength\";\n sport: string;\n title: string;\n startedAt: string;\n endedAt: string | null;\n providerExtras: unknown | null;\n};\n\nexport type DumpEnduranceWorkout = DumpWorkoutBase & {\n type: \"endurance\";\n durationSeconds: number;\n distanceMeters: number;\n elevationGainMeters: number;\n elevationLossMeters: number;\n startLocation: StartLocation | null;\n calories: number;\n averageHeartRate: number;\n maxHeartRate: number;\n averageRunningCadenceStepsPerMinute: number;\n averageStrideLengthCentimeters: number;\n averagePaceMinutesPerKilometer: string;\n fastestPaceMinutesPerKilometer: string;\n activityMetrics: ActivityMetric[];\n};\n\nexport type DumpStrengthWorkout = DumpWorkoutBase & {\n type: \"strength\";\n exercises: DumpStrengthExercise[];\n};\n\nexport type DumpStrengthExercise = StrengthExercise;\n"]}
@@ -0,0 +1,2 @@
1
+ export const providers = ["garmin", "hevy"];
2
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/domain/provider.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAU,CAAC","sourcesContent":["export const providers = [\"garmin\", \"hevy\"] as const;\n\nexport type Provider = (typeof providers)[number];\n\nexport type ProviderSyncResult = {\n newWorkoutCount: number;\n};\n"]}
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ export const ActivityMetricSchema = z.tuple([
3
+ z.number(),
4
+ z.number(),
5
+ z.string().min(1),
6
+ ]);
7
+ export const ActivityMetricsSchema = z.array(ActivityMetricSchema);
8
+ export const StartLocationSchema = z.tuple([z.number(), z.number()]);
9
+ export const StrengthSetSchema = z
10
+ .object({
11
+ weightKg: z.number(),
12
+ reps: z.number(),
13
+ durationSeconds: z.number(),
14
+ })
15
+ .strict();
16
+ export const StrengthExerciseSchema = z
17
+ .object({
18
+ title: z.string().min(1),
19
+ sets: z.array(StrengthSetSchema),
20
+ })
21
+ .strict();
22
+ export const StrengthExercisesSchema = z.array(StrengthExerciseSchema);
23
+ //# sourceMappingURL=workout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workout.js","sourceRoot":"","sources":["../../src/domain/workout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC;IAC1C,CAAC,CAAC,MAAM,EAAE;IACV,CAAC,CAAC,MAAM,EAAE;IACV,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAInE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAqBrE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE;CAC5B,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC;KACpC,MAAM,CAAC;IACN,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;CACjC,CAAC;KACD,MAAM,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC","sourcesContent":["import { z } from \"zod\";\nimport type { Provider } from \"./provider.js\";\n\nexport type WorkoutType = \"endurance\" | \"strength\";\n\nexport type Workout = {\n id: string;\n provider: Provider;\n providerId: string;\n type: WorkoutType;\n sport: string;\n title: string;\n startedAtMs: number;\n endedAtMs: number | null;\n sourceJson: string;\n providerExtrasJson: string | null;\n};\n\nexport const ActivityMetricSchema = z.tuple([\n z.number(),\n z.number(),\n z.string().min(1),\n]);\n\nexport const ActivityMetricsSchema = z.array(ActivityMetricSchema);\n\nexport type ActivityMetric = z.infer<typeof ActivityMetricSchema>;\n\nexport const StartLocationSchema = z.tuple([z.number(), z.number()]);\n\nexport type StartLocation = z.infer<typeof StartLocationSchema>;\n\nexport type EnduranceMetricsRow = {\n workoutId: string;\n durationSeconds: number;\n distanceMeters: number;\n elevationGainMeters: number;\n elevationLossMeters: number;\n startLocation: StartLocation | null;\n calories: number;\n averageHeartRate: number;\n maxHeartRate: number;\n averageRunningCadenceStepsPerMinute: number;\n averageStrideLengthCentimeters: number;\n averagePaceMinutesPerKilometer: string;\n fastestPaceMinutesPerKilometer: string;\n activityMetricsJson: string;\n};\n\nexport const StrengthSetSchema = z\n .object({\n weightKg: z.number(),\n reps: z.number(),\n durationSeconds: z.number(),\n })\n .strict();\n\nexport const StrengthExerciseSchema = z\n .object({\n title: z.string().min(1),\n sets: z.array(StrengthSetSchema),\n })\n .strict();\n\nexport const StrengthExercisesSchema = z.array(StrengthExerciseSchema);\n\nexport type StrengthSet = z.infer<typeof StrengthSetSchema>;\n\nexport type StrengthExercise = z.infer<typeof StrengthExerciseSchema>;\n\nexport type StrengthMetricsRow = {\n workoutId: string;\n exercisesJson: string;\n};\n\nexport type WorkoutWithMetrics =\n | {\n type: \"endurance\";\n workout: Workout & { type: \"endurance\" };\n enduranceMetrics: EnduranceMetricsRow;\n }\n | {\n type: \"strength\";\n workout: Workout & { type: \"strength\" };\n strengthMetrics: StrengthMetricsRow;\n };\n"]}
@@ -0,0 +1,38 @@
1
+ import garminConnectPackage from "garmin-connect";
2
+ import { parseSchema } from "../../utils/parse.js";
3
+ import { GarminApiActivitiesSchema, GarminApiDetailsResponseSchema, GarminApiExerciseSetsResponseSchema, } from "./types.js";
4
+ const { GarminConnect } = garminConnectPackage;
5
+ const garminActivityDetailsUrl = (activityId) => `https://connectapi.garmin.com/activity-service/activity/${activityId}/details`;
6
+ const garminExerciseSetsUrl = (activityId) => `https://connectapi.garmin.com/activity-service/activity/${activityId}/exerciseSets`;
7
+ export class GarminClient {
8
+ #client;
9
+ constructor(client) {
10
+ this.#client = client;
11
+ }
12
+ static async login(username, password) {
13
+ const client = new GarminConnect({ username, password });
14
+ await client.login();
15
+ return client.exportToken();
16
+ }
17
+ static fromTokens(tokens) {
18
+ const client = new GarminConnect({ username: "", password: "" });
19
+ client.loadToken(tokens.oauth1, tokens.oauth2);
20
+ return new GarminClient(client);
21
+ }
22
+ async getActivities(start, limit) {
23
+ return parseSchema(GarminApiActivitiesSchema, await this.#client.getActivities(start, limit), "Garmin activities response");
24
+ }
25
+ async getActivityDetails(activityId) {
26
+ return parseSchema(GarminApiDetailsResponseSchema,
27
+ // garmin-connect does not expose useful types for these raw endpoint responses.
28
+ // Treat them as boundary values and parse with zod before using them.
29
+ await this.#client.get(garminActivityDetailsUrl(activityId)), `Garmin activity ${activityId} details response`);
30
+ }
31
+ async getExerciseSets(activityId) {
32
+ return parseSchema(GarminApiExerciseSetsResponseSchema,
33
+ // garmin-connect does not expose useful types for these raw endpoint responses.
34
+ // Treat them as boundary values and parse with zod before using them.
35
+ await this.#client.get(garminExerciseSetsUrl(activityId)), `Garmin activity ${activityId} exercise sets response`);
36
+ }
37
+ }
38
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/providers/garmin/client.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAOnD,OAAO,EACL,yBAAyB,EACzB,8BAA8B,EAC9B,mCAAmC,GACpC,MAAM,YAAY,CAAC;AAMpB,MAAM,EAAE,aAAa,EAAE,GAAG,oBAEzB,CAAC;AAEF,MAAM,wBAAwB,GAAG,CAAC,UAAkB,EAAE,EAAE,CACtD,2DAA2D,UAAU,UAAU,CAAC;AAElF,MAAM,qBAAqB,GAAG,CAAC,UAAkB,EAAE,EAAE,CACnD,2DAA2D,UAAU,eAAe,CAAC;AAEvF,MAAM,OAAO,YAAY;IACd,OAAO,CAAwB;IAExC,YAAoB,MAA6B;QAC/C,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,KAAK,CAChB,QAAgB,EAChB,QAAgB;QAEhB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC;IAC9B,CAAC;IAED,MAAM,CAAC,UAAU,CAAC,MAAoB;QACpC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/C,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,KAAa,EACb,KAAa;QAEb,OAAO,WAAW,CAChB,yBAAyB,EACzB,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,EAC9C,4BAA4B,CAC7B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,kBAAkB,CACtB,UAAkB;QAElB,OAAO,WAAW,CAChB,8BAA8B;QAC9B,gFAAgF;QAChF,sEAAsE;QACtE,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAU,wBAAwB,CAAC,UAAU,CAAC,CAAC,EACrE,mBAAmB,UAAU,mBAAmB,CACjD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,UAAkB;QAElB,OAAO,WAAW,CAChB,mCAAmC;QACnC,gFAAgF;QAChF,sEAAsE;QACtE,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAU,qBAAqB,CAAC,UAAU,CAAC,CAAC,EAClE,mBAAmB,UAAU,yBAAyB,CACvD,CAAC;IACJ,CAAC;CACF","sourcesContent":["import garminConnectPackage from \"garmin-connect\";\nimport { parseSchema } from \"../../utils/parse.js\";\nimport type {\n GarminApiActivity,\n GarminApiDetailsResponse,\n GarminApiExerciseSetsResponse,\n GarminTokens,\n} from \"./types.js\";\nimport {\n GarminApiActivitiesSchema,\n GarminApiDetailsResponseSchema,\n GarminApiExerciseSetsResponseSchema,\n} from \"./types.js\";\n\ntype GarminConnectConstructor =\n typeof import(\"garmin-connect/dist/garmin/GarminConnect.js\")[\"default\"];\ntype GarminConnectInstance = InstanceType<GarminConnectConstructor>;\n\nconst { GarminConnect } = garminConnectPackage as unknown as {\n GarminConnect: GarminConnectConstructor;\n};\n\nconst garminActivityDetailsUrl = (activityId: number) =>\n `https://connectapi.garmin.com/activity-service/activity/${activityId}/details`;\n\nconst garminExerciseSetsUrl = (activityId: number) =>\n `https://connectapi.garmin.com/activity-service/activity/${activityId}/exerciseSets`;\n\nexport class GarminClient {\n readonly #client: GarminConnectInstance;\n\n private constructor(client: GarminConnectInstance) {\n this.#client = client;\n }\n\n static async login(\n username: string,\n password: string,\n ): Promise<GarminTokens> {\n const client = new GarminConnect({ username, password });\n await client.login();\n return client.exportToken();\n }\n\n static fromTokens(tokens: GarminTokens): GarminClient {\n const client = new GarminConnect({ username: \"\", password: \"\" });\n client.loadToken(tokens.oauth1, tokens.oauth2);\n return new GarminClient(client);\n }\n\n async getActivities(\n start: number,\n limit: number,\n ): Promise<GarminApiActivity[]> {\n return parseSchema(\n GarminApiActivitiesSchema,\n await this.#client.getActivities(start, limit),\n \"Garmin activities response\",\n );\n }\n\n async getActivityDetails(\n activityId: number,\n ): Promise<GarminApiDetailsResponse> {\n return parseSchema(\n GarminApiDetailsResponseSchema,\n // garmin-connect does not expose useful types for these raw endpoint responses.\n // Treat them as boundary values and parse with zod before using them.\n await this.#client.get<unknown>(garminActivityDetailsUrl(activityId)),\n `Garmin activity ${activityId} details response`,\n );\n }\n\n async getExerciseSets(\n activityId: number,\n ): Promise<GarminApiExerciseSetsResponse> {\n return parseSchema(\n GarminApiExerciseSetsResponseSchema,\n // garmin-connect does not expose useful types for these raw endpoint responses.\n // Treat them as boundary values and parse with zod before using them.\n await this.#client.get<unknown>(garminExerciseSetsUrl(activityId)),\n `Garmin activity ${activityId} exercise sets response`,\n );\n }\n}\n"]}
@@ -0,0 +1,145 @@
1
+ import { ActivityMetricsSchema, StrengthExercisesSchema, } from "../../domain/workout.js";
2
+ import { metricNumber, optionalFiniteNumber, parseSchema, stringifyJson, } from "../../utils/parse.js";
3
+ import { speedToPaceMinutesPerKilometer } from "../../utils/running.js";
4
+ export function normalizeGarminWorkoutSource(source) {
5
+ const activity = source.activity;
6
+ const activityId = activity.activityId;
7
+ const sport = activity.activityType.typeKey;
8
+ const startedAtMs = parseGarminGmtTimestamp(activity.startTimeGMT);
9
+ const durationSeconds = metricNumber(activity.duration);
10
+ const endedAtMs = startedAtMs + durationSeconds * 1000;
11
+ const type = garminWorkoutType(sport);
12
+ const workoutId = `garmin:${activityId}`;
13
+ const workout = {
14
+ id: workoutId,
15
+ provider: "garmin",
16
+ providerId: String(activityId),
17
+ type,
18
+ sport,
19
+ title: activity.activityName,
20
+ startedAtMs,
21
+ endedAtMs,
22
+ sourceJson: stringifyJson(source),
23
+ providerExtrasJson: garminProviderExtrasJson(type, source),
24
+ };
25
+ if (type === "endurance") {
26
+ return {
27
+ type: "endurance",
28
+ workout: { ...workout, type: "endurance" },
29
+ enduranceMetrics: {
30
+ workoutId,
31
+ durationSeconds,
32
+ distanceMeters: metricNumber(activity.distance),
33
+ elevationGainMeters: metricNumber(activity.elevationGain),
34
+ elevationLossMeters: metricNumber(activity.elevationLoss),
35
+ startLocation: garminStartLocation(activity.startLatitude, activity.startLongitude),
36
+ calories: metricNumber(activity.calories),
37
+ averageHeartRate: metricNumber(activity.averageHR),
38
+ maxHeartRate: metricNumber(activity.maxHR),
39
+ averageRunningCadenceStepsPerMinute: metricNumber(activity.averageRunningCadenceInStepsPerMinute),
40
+ averageStrideLengthCentimeters: metricNumber(activity.avgStrideLength),
41
+ averagePaceMinutesPerKilometer: speedToPaceMinutesPerKilometer(metricNumber(activity.averageSpeed)),
42
+ fastestPaceMinutesPerKilometer: speedToPaceMinutesPerKilometer(metricNumber(activity.maxSpeed)),
43
+ activityMetricsJson: stringifyJson(parseSchema(ActivityMetricsSchema, extractActivityMetrics(source), "Garmin activity metrics")),
44
+ },
45
+ };
46
+ }
47
+ return {
48
+ type: "strength",
49
+ workout: { ...workout, type: "strength" },
50
+ strengthMetrics: normalizeGarminStrengthMetrics(workoutId, source.exerciseSets.exerciseSets),
51
+ };
52
+ }
53
+ function normalizeGarminStrengthMetrics(workoutId, exerciseSets) {
54
+ const exerciseGroups = new Map();
55
+ for (const set of exerciseSets) {
56
+ if (set.setType === "REST") {
57
+ continue;
58
+ }
59
+ const exerciseIndex = optionalFiniteNumber(set.wktStepIndex) ?? set.setIndex;
60
+ const existing = exerciseGroups.get(exerciseIndex);
61
+ const exercise = existing ??
62
+ {
63
+ title: bestExerciseName(set),
64
+ sets: [],
65
+ };
66
+ exercise.sets.push({
67
+ weightKg: garminWeightKg(set.weight),
68
+ reps: metricNumber(set.repetitionCount),
69
+ durationSeconds: metricNumber(set.duration),
70
+ });
71
+ exerciseGroups.set(exerciseIndex, exercise);
72
+ }
73
+ const exercises = [...exerciseGroups.entries()]
74
+ .sort(([leftIndex], [rightIndex]) => leftIndex - rightIndex)
75
+ .map(([, exercise]) => exercise);
76
+ return {
77
+ workoutId,
78
+ exercisesJson: stringifyJson(parseSchema(StrengthExercisesSchema, exercises, "Garmin strength metrics")),
79
+ };
80
+ }
81
+ function bestExerciseName(set) {
82
+ const exercises = set.exercises;
83
+ if (exercises.length === 0) {
84
+ return "Unknown";
85
+ }
86
+ const sorted = [...exercises].sort((a, b) => b.probability - a.probability);
87
+ const best = sorted[0];
88
+ return best && best.name.length > 0 ? best.name : "Unknown";
89
+ }
90
+ function garminWorkoutType(sport) {
91
+ return sport.toLowerCase().includes("strength") ? "strength" : "endurance";
92
+ }
93
+ function garminProviderExtrasJson(type, source) {
94
+ const activity = source.activity;
95
+ if (type === "strength") {
96
+ return stringifyJson({
97
+ calories: metricNumber(activity.calories),
98
+ averageHeartRate: metricNumber(activity.averageHR),
99
+ maxHeartRate: metricNumber(activity.maxHR),
100
+ });
101
+ }
102
+ return null;
103
+ }
104
+ function extractActivityMetrics(source) {
105
+ const heartRateDescriptor = source.details.metricDescriptors.find((descriptor) => descriptor.key === "directHeartRate");
106
+ const elapsedDescriptor = source.details.metricDescriptors.find((descriptor) => descriptor.key === "sumElapsedDuration");
107
+ const speedDescriptor = source.details.metricDescriptors.find((descriptor) => descriptor.key === "directSpeed");
108
+ if (!heartRateDescriptor || !elapsedDescriptor) {
109
+ return [];
110
+ }
111
+ const activityMetrics = [];
112
+ for (const metric of source.details.activityDetailMetrics) {
113
+ const heartRate = metric.metrics[heartRateDescriptor.metricsIndex];
114
+ const secondsElapsed = metric.metrics[elapsedDescriptor.metricsIndex];
115
+ if (typeof heartRate === "number" && typeof secondsElapsed === "number") {
116
+ const speed = speedDescriptor === undefined
117
+ ? 0
118
+ : metricNumber(metric.metrics[speedDescriptor.metricsIndex]);
119
+ activityMetrics.push([
120
+ secondsElapsed,
121
+ heartRate,
122
+ speedToPaceMinutesPerKilometer(speed),
123
+ ]);
124
+ }
125
+ }
126
+ return activityMetrics;
127
+ }
128
+ function parseGarminGmtTimestamp(value) {
129
+ const ms = Date.parse(`${value.replace(" ", "T")}Z`);
130
+ if (!Number.isFinite(ms)) {
131
+ throw new Error(`Invalid Garmin GMT timestamp "${value}"`);
132
+ }
133
+ return ms;
134
+ }
135
+ function garminWeightKg(value) {
136
+ return metricNumber(value) / 1000;
137
+ }
138
+ function garminStartLocation(latitude, longitude) {
139
+ const startLatitude = optionalFiniteNumber(latitude);
140
+ const startLongitude = optionalFiniteNumber(longitude);
141
+ return startLatitude === undefined || startLongitude === undefined
142
+ ? null
143
+ : [startLatitude, startLongitude];
144
+ }
145
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../src/providers/garmin/normalize.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ActivityMetric,\n StartLocation,\n StrengthExercise,\n StrengthMetricsRow,\n Workout,\n WorkoutType,\n WorkoutWithMetrics,\n} from \"../../domain/workout.js\";\nimport {\n ActivityMetricsSchema,\n StrengthExercisesSchema,\n} from \"../../domain/workout.js\";\nimport {\n metricNumber,\n optionalFiniteNumber,\n parseSchema,\n stringifyJson,\n} from \"../../utils/parse.js\";\nimport { speedToPaceMinutesPerKilometer } from \"../../utils/running.js\";\nimport type { GarminExerciseSet, GarminWorkoutSource } from \"./types.js\";\n\nexport function normalizeGarminWorkoutSource(\n source: GarminWorkoutSource,\n): WorkoutWithMetrics {\n const activity = source.activity;\n const activityId = activity.activityId;\n const sport = activity.activityType.typeKey;\n const startedAtMs = parseGarminGmtTimestamp(activity.startTimeGMT);\n const durationSeconds = metricNumber(activity.duration);\n const endedAtMs = startedAtMs + durationSeconds * 1000;\n const type = garminWorkoutType(sport);\n const workoutId = `garmin:${activityId}`;\n const workout = {\n id: workoutId,\n provider: \"garmin\",\n providerId: String(activityId),\n type,\n sport,\n title: activity.activityName,\n startedAtMs,\n endedAtMs,\n sourceJson: stringifyJson(source),\n providerExtrasJson: garminProviderExtrasJson(type, source),\n } satisfies Workout;\n\n if (type === \"endurance\") {\n return {\n type: \"endurance\",\n workout: { ...workout, type: \"endurance\" },\n enduranceMetrics: {\n workoutId,\n durationSeconds,\n distanceMeters: metricNumber(activity.distance),\n elevationGainMeters: metricNumber(activity.elevationGain),\n elevationLossMeters: metricNumber(activity.elevationLoss),\n startLocation: garminStartLocation(\n activity.startLatitude,\n activity.startLongitude,\n ),\n calories: metricNumber(activity.calories),\n averageHeartRate: metricNumber(activity.averageHR),\n maxHeartRate: metricNumber(activity.maxHR),\n averageRunningCadenceStepsPerMinute: metricNumber(\n activity.averageRunningCadenceInStepsPerMinute,\n ),\n averageStrideLengthCentimeters: metricNumber(activity.avgStrideLength),\n averagePaceMinutesPerKilometer: speedToPaceMinutesPerKilometer(\n metricNumber(activity.averageSpeed),\n ),\n fastestPaceMinutesPerKilometer: speedToPaceMinutesPerKilometer(\n metricNumber(activity.maxSpeed),\n ),\n activityMetricsJson: stringifyJson(\n parseSchema(\n ActivityMetricsSchema,\n extractActivityMetrics(source),\n \"Garmin activity metrics\",\n ),\n ),\n },\n };\n }\n\n return {\n type: \"strength\",\n workout: { ...workout, type: \"strength\" },\n strengthMetrics: normalizeGarminStrengthMetrics(\n workoutId,\n source.exerciseSets.exerciseSets,\n ),\n };\n}\n\nfunction normalizeGarminStrengthMetrics(\n workoutId: string,\n exerciseSets: GarminExerciseSet[],\n): StrengthMetricsRow {\n const exerciseGroups = new Map<number, StrengthExercise>();\n\n for (const set of exerciseSets) {\n if (set.setType === \"REST\") {\n continue;\n }\n\n const exerciseIndex =\n optionalFiniteNumber(set.wktStepIndex) ?? set.setIndex;\n const existing = exerciseGroups.get(exerciseIndex);\n const exercise =\n existing ??\n ({\n title: bestExerciseName(set),\n sets: [],\n } satisfies StrengthExercise);\n\n exercise.sets.push({\n weightKg: garminWeightKg(set.weight),\n reps: metricNumber(set.repetitionCount),\n durationSeconds: metricNumber(set.duration),\n });\n\n exerciseGroups.set(exerciseIndex, exercise);\n }\n\n const exercises = [...exerciseGroups.entries()]\n .sort(([leftIndex], [rightIndex]) => leftIndex - rightIndex)\n .map(([, exercise]) => exercise);\n\n return {\n workoutId,\n exercisesJson: stringifyJson(\n parseSchema(\n StrengthExercisesSchema,\n exercises,\n \"Garmin strength metrics\",\n ),\n ),\n };\n}\n\nfunction bestExerciseName(set: GarminExerciseSet): string {\n const exercises = set.exercises;\n if (exercises.length === 0) {\n return \"Unknown\";\n }\n\n const sorted = [...exercises].sort((a, b) => b.probability - a.probability);\n const best = sorted[0];\n return best && best.name.length > 0 ? best.name : \"Unknown\";\n}\n\nfunction garminWorkoutType(sport: string): WorkoutType {\n return sport.toLowerCase().includes(\"strength\") ? \"strength\" : \"endurance\";\n}\n\nfunction garminProviderExtrasJson(\n type: WorkoutType,\n source: GarminWorkoutSource,\n): string | null {\n const activity = source.activity;\n if (type === \"strength\") {\n return stringifyJson({\n calories: metricNumber(activity.calories),\n averageHeartRate: metricNumber(activity.averageHR),\n maxHeartRate: metricNumber(activity.maxHR),\n });\n }\n\n return null;\n}\n\nfunction extractActivityMetrics(source: GarminWorkoutSource): ActivityMetric[] {\n const heartRateDescriptor = source.details.metricDescriptors.find(\n (descriptor) => descriptor.key === \"directHeartRate\",\n );\n const elapsedDescriptor = source.details.metricDescriptors.find(\n (descriptor) => descriptor.key === \"sumElapsedDuration\",\n );\n const speedDescriptor = source.details.metricDescriptors.find(\n (descriptor) => descriptor.key === \"directSpeed\",\n );\n\n if (!heartRateDescriptor || !elapsedDescriptor) {\n return [];\n }\n\n const activityMetrics: ActivityMetric[] = [];\n for (const metric of source.details.activityDetailMetrics) {\n const heartRate = metric.metrics[heartRateDescriptor.metricsIndex];\n const secondsElapsed = metric.metrics[elapsedDescriptor.metricsIndex];\n if (typeof heartRate === \"number\" && typeof secondsElapsed === \"number\") {\n const speed =\n speedDescriptor === undefined\n ? 0\n : metricNumber(metric.metrics[speedDescriptor.metricsIndex]);\n activityMetrics.push([\n secondsElapsed,\n heartRate,\n speedToPaceMinutesPerKilometer(speed),\n ]);\n }\n }\n\n return activityMetrics;\n}\n\nfunction parseGarminGmtTimestamp(value: string): number {\n const ms = Date.parse(`${value.replace(\" \", \"T\")}Z`);\n if (!Number.isFinite(ms)) {\n throw new Error(`Invalid Garmin GMT timestamp \"${value}\"`);\n }\n return ms;\n}\n\nfunction garminWeightKg(value: number | null | undefined): number {\n return metricNumber(value) / 1000;\n}\n\nfunction garminStartLocation(\n latitude: number | null | undefined,\n longitude: number | null | undefined,\n): StartLocation | null {\n const startLatitude = optionalFiniteNumber(latitude);\n const startLongitude = optionalFiniteNumber(longitude);\n return startLatitude === undefined || startLongitude === undefined\n ? null\n : [startLatitude, startLongitude];\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { optionalFiniteNumber, parseSchema } from "../../utils/parse.js";
2
+ import { GarminExerciseSetsResponseSchema } from "./types.js";
3
+ export async function fetchFullGarminWorkout(client, activity) {
4
+ const details = await client.getActivityDetails(activity.activityId);
5
+ const exerciseSets = normalizeExerciseSetsResponse(await client.getExerciseSets(activity.activityId));
6
+ return {
7
+ activity,
8
+ details,
9
+ exerciseSets,
10
+ };
11
+ }
12
+ export function normalizeExerciseSetsResponse(response) {
13
+ return parseSchema(GarminExerciseSetsResponseSchema, {
14
+ ...response,
15
+ exerciseSets: (response.exerciseSets ?? []).map((set, sourceIndex) => normalizeApiExerciseSet(set, sourceIndex)),
16
+ }, "normalized Garmin exercise sets");
17
+ }
18
+ function normalizeApiExerciseSet(set, sourceIndex) {
19
+ const repetitionCount = optionalFiniteNumber(set.repetitionCount);
20
+ const weight = optionalFiniteNumber(set.weight);
21
+ const wktStepIndex = optionalFiniteNumber(set.wktStepIndex);
22
+ return {
23
+ ...set,
24
+ exercises: (set.exercises ?? []).map(normalizeApiExercise),
25
+ duration: set.duration,
26
+ ...(repetitionCount === undefined ? {} : { repetitionCount }),
27
+ ...(weight === undefined ? {} : { weight }),
28
+ setType: set.setType,
29
+ startTime: set.startTime,
30
+ ...(wktStepIndex === undefined ? {} : { wktStepIndex }),
31
+ setIndex: optionalFiniteNumber(set.messageIndex) ?? sourceIndex,
32
+ };
33
+ }
34
+ function normalizeApiExercise(exercise) {
35
+ return {
36
+ ...exercise,
37
+ category: typeof exercise.category === "string" && exercise.category.length > 0
38
+ ? exercise.category
39
+ : "Unknown",
40
+ name: typeof exercise.name === "string" && exercise.name.length > 0
41
+ ? exercise.name
42
+ : "Unknown",
43
+ probability: typeof exercise.probability === "number" &&
44
+ Number.isFinite(exercise.probability)
45
+ ? exercise.probability
46
+ : 0,
47
+ };
48
+ }
49
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../../src/providers/garmin/source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWzE,OAAO,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAoB,EACpB,QAA2B;IAE3B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACrE,MAAM,YAAY,GAAG,6BAA6B,CAChD,MAAM,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,CAClD,CAAC;IAEF,OAAO;QACL,QAAQ;QACR,OAAO;QACP,YAAY;KACb,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,QAAuC;IAEvC,OAAO,WAAW,CAChB,gCAAgC,EAChC;QACE,GAAG,QAAQ;QACX,YAAY,EAAE,CAAC,QAAQ,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,CACnE,uBAAuB,CAAC,GAAG,EAAE,WAAW,CAAC,CAC1C;KACF,EACD,iCAAiC,CAClC,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAC9B,GAAyB,EACzB,WAAmB;IAEnB,MAAM,eAAe,GAAG,oBAAoB,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,YAAY,GAAG,oBAAoB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAE5D,OAAO;QACL,GAAG,GAAG;QACN,SAAS,EAAE,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAC1D,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,GAAG,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC;QAC7D,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3C,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;QACvD,QAAQ,EAAE,oBAAoB,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,WAAW;KAChE,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,QAA2B;IACvD,OAAO;QACL,GAAG,QAAQ;QACX,QAAQ,EACN,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnE,CAAC,CAAC,QAAQ,CAAC,QAAQ;YACnB,CAAC,CAAC,SAAS;QACf,IAAI,EACF,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAC3D,CAAC,CAAC,QAAQ,CAAC,IAAI;YACf,CAAC,CAAC,SAAS;QACf,WAAW,EACT,OAAO,QAAQ,CAAC,WAAW,KAAK,QAAQ;YACxC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;YACnC,CAAC,CAAC,QAAQ,CAAC,WAAW;YACtB,CAAC,CAAC,CAAC;KACR,CAAC;AACJ,CAAC","sourcesContent":["import { optionalFiniteNumber, parseSchema } from \"../../utils/parse.js\";\nimport type { GarminClient } from \"./client.js\";\nimport type {\n GarminApiActivity,\n GarminApiExercise,\n GarminApiExerciseSet,\n GarminApiExerciseSetsResponse,\n GarminExercise,\n GarminExerciseSetsResponse,\n GarminWorkoutSource,\n} from \"./types.js\";\nimport { GarminExerciseSetsResponseSchema } from \"./types.js\";\n\nexport async function fetchFullGarminWorkout(\n client: GarminClient,\n activity: GarminApiActivity,\n): Promise<GarminWorkoutSource> {\n const details = await client.getActivityDetails(activity.activityId);\n const exerciseSets = normalizeExerciseSetsResponse(\n await client.getExerciseSets(activity.activityId),\n );\n\n return {\n activity,\n details,\n exerciseSets,\n };\n}\n\nexport function normalizeExerciseSetsResponse(\n response: GarminApiExerciseSetsResponse,\n): GarminExerciseSetsResponse {\n return parseSchema(\n GarminExerciseSetsResponseSchema,\n {\n ...response,\n exerciseSets: (response.exerciseSets ?? []).map((set, sourceIndex) =>\n normalizeApiExerciseSet(set, sourceIndex),\n ),\n },\n \"normalized Garmin exercise sets\",\n );\n}\n\nfunction normalizeApiExerciseSet(\n set: GarminApiExerciseSet,\n sourceIndex: number,\n): GarminExerciseSetsResponse[\"exerciseSets\"][number] {\n const repetitionCount = optionalFiniteNumber(set.repetitionCount);\n const weight = optionalFiniteNumber(set.weight);\n const wktStepIndex = optionalFiniteNumber(set.wktStepIndex);\n\n return {\n ...set,\n exercises: (set.exercises ?? []).map(normalizeApiExercise),\n duration: set.duration,\n ...(repetitionCount === undefined ? {} : { repetitionCount }),\n ...(weight === undefined ? {} : { weight }),\n setType: set.setType,\n startTime: set.startTime,\n ...(wktStepIndex === undefined ? {} : { wktStepIndex }),\n setIndex: optionalFiniteNumber(set.messageIndex) ?? sourceIndex,\n };\n}\n\nfunction normalizeApiExercise(exercise: GarminApiExercise): GarminExercise {\n return {\n ...exercise,\n category:\n typeof exercise.category === \"string\" && exercise.category.length > 0\n ? exercise.category\n : \"Unknown\",\n name:\n typeof exercise.name === \"string\" && exercise.name.length > 0\n ? exercise.name\n : \"Unknown\",\n probability:\n typeof exercise.probability === \"number\" &&\n Number.isFinite(exercise.probability)\n ? exercise.probability\n : 0,\n };\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import { updateProviderCursor, } from "../../db/provider-state.js";
2
+ import { upsertNormalizedWorkout } from "../../db/workouts.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { parseJson, parseSchema, stringifyJson } from "../../utils/parse.js";
5
+ import { GarminClient } from "./client.js";
6
+ import { normalizeGarminWorkoutSource } from "./normalize.js";
7
+ import { fetchFullGarminWorkout } from "./source.js";
8
+ import { GarminCursorSchema, GarminTokensSchema } from "./types.js";
9
+ const batchSize = 100;
10
+ export async function syncGarmin(db, state) {
11
+ const tokens = parseGarminTokens(state.credentialsJson);
12
+ const cursor = parseGarminCursor(state.cursorJson);
13
+ const client = GarminClient.fromTokens(tokens);
14
+ const newActivities = await getActivitiesToSync(client, cursor.highestSyncedActivityId);
15
+ const sortedNewActivities = [...newActivities].sort((a, b) => a.activityId - b.activityId);
16
+ if (sortedNewActivities.length > 0) {
17
+ logger.info(`Garmin found ${formatWorkoutCount(sortedNewActivities.length)} to sync.`);
18
+ }
19
+ let newWorkoutCount = 0;
20
+ let highestSyncedActivityId = cursor.highestSyncedActivityId;
21
+ const fullWorkoutByActivityId = new Map();
22
+ for (const [index, activity] of sortedNewActivities.entries()) {
23
+ logger.debug(`Garmin fetching ${index + 1}/${sortedNewActivities.length}: ${activity.activityName}`);
24
+ fullWorkoutByActivityId.set(activity.activityId, await fetchFullGarminWorkout(client, activity));
25
+ }
26
+ const transaction = db.transaction(() => {
27
+ for (const activity of sortedNewActivities) {
28
+ const fullWorkout = fullWorkoutByActivityId.get(activity.activityId);
29
+ if (!fullWorkout) {
30
+ throw new Error(`Missing full Garmin workout for activity ${activity.activityId}`);
31
+ }
32
+ const rows = normalizeGarminWorkoutSource(fullWorkout);
33
+ if (upsertNormalizedWorkout(db, rows)) {
34
+ newWorkoutCount += 1;
35
+ }
36
+ highestSyncedActivityId = Math.max(highestSyncedActivityId, activity.activityId);
37
+ }
38
+ updateProviderCursor(db, "garmin", stringifyJson({
39
+ version: 1,
40
+ highestSyncedActivityId,
41
+ }), Date.now());
42
+ });
43
+ transaction();
44
+ return { newWorkoutCount };
45
+ }
46
+ function formatWorkoutCount(count) {
47
+ return `${count} new ${count === 1 ? "workout" : "workouts"}`;
48
+ }
49
+ async function getActivitiesToSync(client, highestSyncedActivityId) {
50
+ const activities = [];
51
+ let start = 0;
52
+ while (true) {
53
+ const batch = await client.getActivities(start, batchSize);
54
+ const lastSyncedIndex = batch.findIndex((activity) => activity.activityId <= highestSyncedActivityId);
55
+ if (lastSyncedIndex !== -1) {
56
+ activities.push(...batch.slice(0, lastSyncedIndex));
57
+ return activities;
58
+ }
59
+ activities.push(...batch);
60
+ if (batch.length < batchSize) {
61
+ return activities;
62
+ }
63
+ start += batchSize;
64
+ }
65
+ }
66
+ function parseGarminTokens(json) {
67
+ return parseSchema(GarminTokensSchema, parseJson(json, "garmin credentials_json"), "Garmin credentials");
68
+ }
69
+ function parseGarminCursor(json) {
70
+ return parseSchema(GarminCursorSchema, parseJson(json, "garmin cursor_json"), "Garmin cursor");
71
+ }
72
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/providers/garmin/sync.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,oBAAoB,GACrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAOrD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAqB,EACrB,KAAoB;IAEpB,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAG,MAAM,mBAAmB,CAC7C,MAAM,EACN,MAAM,CAAC,uBAAuB,CAC/B,CAAC;IACF,MAAM,mBAAmB,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,IAAI,CACjD,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CACtC,CAAC;IACF,IAAI,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,IAAI,CACT,gBAAgB,kBAAkB,CAAC,mBAAmB,CAAC,MAAM,CAAC,WAAW,CAC1E,CAAC;IACJ,CAAC;IACD,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,IAAI,uBAAuB,GAAG,MAAM,CAAC,uBAAuB,CAAC;IAE7D,MAAM,uBAAuB,GAAG,IAAI,GAAG,EAA+B,CAAC;IACvE,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9D,MAAM,CAAC,KAAK,CACV,mBAAmB,KAAK,GAAG,CAAC,IAAI,mBAAmB,CAAC,MAAM,KAAK,QAAQ,CAAC,YAAY,EAAE,CACvF,CAAC;QACF,uBAAuB,CAAC,GAAG,CACzB,QAAQ,CAAC,UAAU,EACnB,MAAM,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAC/C,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QACtC,KAAK,MAAM,QAAQ,IAAI,mBAAmB,EAAE,CAAC;YAC3C,MAAM,WAAW,GAAG,uBAAuB,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrE,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CACb,4CAA4C,QAAQ,CAAC,UAAU,EAAE,CAClE,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,4BAA4B,CAAC,WAAW,CAAC,CAAC;YACvD,IAAI,uBAAuB,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;gBACtC,eAAe,IAAI,CAAC,CAAC;YACvB,CAAC;YACD,uBAAuB,GAAG,IAAI,CAAC,GAAG,CAChC,uBAAuB,EACvB,QAAQ,CAAC,UAAU,CACpB,CAAC;QACJ,CAAC;QAED,oBAAoB,CAClB,EAAE,EACF,QAAQ,EACR,aAAa,CAAC;YACZ,OAAO,EAAE,CAAC;YACV,uBAAuB;SACD,CAAC,EACzB,IAAI,CAAC,GAAG,EAAE,CACX,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,eAAe,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,GAAG,KAAK,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,MAAoB,EACpB,uBAA+B;IAE/B,MAAM,UAAU,GAAwB,EAAE,CAAC;IAC3C,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAC3D,MAAM,eAAe,GAAG,KAAK,CAAC,SAAS,CACrC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,IAAI,uBAAuB,CAC7D,CAAC;QAEF,IAAI,eAAe,KAAK,CAAC,CAAC,EAAE,CAAC;YAC3B,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC;YACpD,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;QAE1B,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YAC7B,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,KAAK,IAAI,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,WAAW,CAChB,kBAAkB,EAClB,SAAS,CAAC,IAAI,EAAE,yBAAyB,CAAC,EAC1C,oBAAoB,CACrB,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,WAAW,CAChB,kBAAkB,EAClB,SAAS,CAAC,IAAI,EAAE,oBAAoB,CAAC,EACrC,eAAe,CAChB,CAAC;AACJ,CAAC","sourcesContent":["import type { HealthlogDatabase } from \"../../db/database.js\";\nimport {\n type ProviderState,\n updateProviderCursor,\n} from \"../../db/provider-state.js\";\nimport { upsertNormalizedWorkout } from \"../../db/workouts.js\";\nimport type { ProviderSyncResult } from \"../../domain/provider.js\";\nimport { logger } from \"../../utils/logger.js\";\nimport { parseJson, parseSchema, stringifyJson } from \"../../utils/parse.js\";\nimport { GarminClient } from \"./client.js\";\nimport { normalizeGarminWorkoutSource } from \"./normalize.js\";\nimport { fetchFullGarminWorkout } from \"./source.js\";\nimport type {\n GarminApiActivity,\n GarminCursor,\n GarminTokens,\n GarminWorkoutSource,\n} from \"./types.js\";\nimport { GarminCursorSchema, GarminTokensSchema } from \"./types.js\";\n\nconst batchSize = 100;\n\nexport async function syncGarmin(\n db: HealthlogDatabase,\n state: ProviderState,\n): Promise<ProviderSyncResult> {\n const tokens = parseGarminTokens(state.credentialsJson);\n const cursor = parseGarminCursor(state.cursorJson);\n const client = GarminClient.fromTokens(tokens);\n const newActivities = await getActivitiesToSync(\n client,\n cursor.highestSyncedActivityId,\n );\n const sortedNewActivities = [...newActivities].sort(\n (a, b) => a.activityId - b.activityId,\n );\n if (sortedNewActivities.length > 0) {\n logger.info(\n `Garmin found ${formatWorkoutCount(sortedNewActivities.length)} to sync.`,\n );\n }\n let newWorkoutCount = 0;\n let highestSyncedActivityId = cursor.highestSyncedActivityId;\n\n const fullWorkoutByActivityId = new Map<number, GarminWorkoutSource>();\n for (const [index, activity] of sortedNewActivities.entries()) {\n logger.debug(\n `Garmin fetching ${index + 1}/${sortedNewActivities.length}: ${activity.activityName}`,\n );\n fullWorkoutByActivityId.set(\n activity.activityId,\n await fetchFullGarminWorkout(client, activity),\n );\n }\n\n const transaction = db.transaction(() => {\n for (const activity of sortedNewActivities) {\n const fullWorkout = fullWorkoutByActivityId.get(activity.activityId);\n if (!fullWorkout) {\n throw new Error(\n `Missing full Garmin workout for activity ${activity.activityId}`,\n );\n }\n const rows = normalizeGarminWorkoutSource(fullWorkout);\n if (upsertNormalizedWorkout(db, rows)) {\n newWorkoutCount += 1;\n }\n highestSyncedActivityId = Math.max(\n highestSyncedActivityId,\n activity.activityId,\n );\n }\n\n updateProviderCursor(\n db,\n \"garmin\",\n stringifyJson({\n version: 1,\n highestSyncedActivityId,\n } satisfies GarminCursor),\n Date.now(),\n );\n });\n\n transaction();\n return { newWorkoutCount };\n}\n\nfunction formatWorkoutCount(count: number): string {\n return `${count} new ${count === 1 ? \"workout\" : \"workouts\"}`;\n}\n\nasync function getActivitiesToSync(\n client: GarminClient,\n highestSyncedActivityId: number,\n): Promise<GarminApiActivity[]> {\n const activities: GarminApiActivity[] = [];\n let start = 0;\n\n while (true) {\n const batch = await client.getActivities(start, batchSize);\n const lastSyncedIndex = batch.findIndex(\n (activity) => activity.activityId <= highestSyncedActivityId,\n );\n\n if (lastSyncedIndex !== -1) {\n activities.push(...batch.slice(0, lastSyncedIndex));\n return activities;\n }\n\n activities.push(...batch);\n\n if (batch.length < batchSize) {\n return activities;\n }\n\n start += batchSize;\n }\n}\n\nfunction parseGarminTokens(json: string): GarminTokens {\n return parseSchema(\n GarminTokensSchema,\n parseJson(json, \"garmin credentials_json\"),\n \"Garmin credentials\",\n );\n}\n\nfunction parseGarminCursor(json: string): GarminCursor {\n return parseSchema(\n GarminCursorSchema,\n parseJson(json, \"garmin cursor_json\"),\n \"Garmin cursor\",\n );\n}\n"]}
@@ -0,0 +1,129 @@
1
+ import { z } from "zod";
2
+ const finiteNumber = z.number();
3
+ const optionalFiniteNumber = finiteNumber.nullish();
4
+ export const GarminTokensSchema = z
5
+ .object({
6
+ oauth1: z
7
+ .object({
8
+ oauth_token: z.string().min(1),
9
+ oauth_token_secret: z.string().min(1),
10
+ })
11
+ .strict(),
12
+ oauth2: z
13
+ .object({
14
+ scope: z.string().min(1),
15
+ jti: z.string().min(1),
16
+ access_token: z.string().min(1),
17
+ token_type: z.string().min(1),
18
+ refresh_token: z.string().min(1),
19
+ expires_in: finiteNumber,
20
+ refresh_token_expires_in: finiteNumber,
21
+ expires_at: finiteNumber,
22
+ refresh_token_expires_at: finiteNumber,
23
+ last_update_date: z.string().min(1),
24
+ expires_date: z.string().min(1),
25
+ })
26
+ .strict(),
27
+ })
28
+ .strict();
29
+ export const GarminApiActivityTypeSchema = z.looseObject({
30
+ typeKey: z.string().min(1),
31
+ });
32
+ export const GarminApiActivitySchema = z.looseObject({
33
+ activityId: finiteNumber,
34
+ activityName: z.string().min(1),
35
+ startTimeGMT: z.string().min(1),
36
+ activityType: GarminApiActivityTypeSchema,
37
+ duration: optionalFiniteNumber,
38
+ distance: optionalFiniteNumber,
39
+ elevationGain: optionalFiniteNumber,
40
+ elevationLoss: optionalFiniteNumber,
41
+ startLatitude: optionalFiniteNumber,
42
+ startLongitude: optionalFiniteNumber,
43
+ calories: optionalFiniteNumber,
44
+ averageHR: optionalFiniteNumber,
45
+ maxHR: optionalFiniteNumber,
46
+ averageRunningCadenceInStepsPerMinute: optionalFiniteNumber,
47
+ avgStrideLength: optionalFiniteNumber,
48
+ averageSpeed: optionalFiniteNumber,
49
+ maxSpeed: optionalFiniteNumber,
50
+ });
51
+ export const GarminApiActivitiesSchema = z.array(GarminApiActivitySchema);
52
+ export const GarminApiExerciseSchema = z.looseObject({
53
+ category: z.string().nullish(),
54
+ name: z.string().nullish(),
55
+ probability: optionalFiniteNumber,
56
+ });
57
+ export const GarminApiExerciseSetSchema = z.looseObject({
58
+ exercises: z.array(GarminApiExerciseSchema).nullish(),
59
+ duration: optionalFiniteNumber,
60
+ repetitionCount: optionalFiniteNumber,
61
+ weight: optionalFiniteNumber,
62
+ setType: z.string().nullish(),
63
+ startTime: z.string().nullish(),
64
+ wktStepIndex: optionalFiniteNumber,
65
+ messageIndex: optionalFiniteNumber,
66
+ });
67
+ export const GarminApiExerciseSetsResponseSchema = z.looseObject({
68
+ activityId: finiteNumber,
69
+ exerciseSets: z.array(GarminApiExerciseSetSchema).nullable(),
70
+ });
71
+ export const GarminExerciseSchema = z.looseObject({
72
+ category: z.string().min(1),
73
+ name: z.string().min(1),
74
+ probability: finiteNumber,
75
+ });
76
+ export const GarminExerciseSetSchema = z.looseObject({
77
+ exercises: z.array(GarminExerciseSchema),
78
+ duration: optionalFiniteNumber,
79
+ repetitionCount: optionalFiniteNumber,
80
+ weight: optionalFiniteNumber,
81
+ setType: z.string().nullish(),
82
+ startTime: z.string().nullish(),
83
+ wktStepIndex: optionalFiniteNumber,
84
+ setIndex: finiteNumber,
85
+ });
86
+ export const GarminExerciseSetsResponseSchema = z.looseObject({
87
+ activityId: finiteNumber,
88
+ exerciseSets: z.array(GarminExerciseSetSchema),
89
+ });
90
+ export const GarminUnitSchema = z.looseObject({
91
+ id: finiteNumber,
92
+ key: z.string(),
93
+ factor: finiteNumber,
94
+ });
95
+ export const GarminMetricDescriptorSchema = z.looseObject({
96
+ metricsIndex: finiteNumber,
97
+ key: z.string(),
98
+ unit: GarminUnitSchema,
99
+ });
100
+ export const GarminActivityDetailMetricSchema = z.looseObject({
101
+ metrics: z.array(finiteNumber.nullable()),
102
+ });
103
+ export const GarminApiDetailsResponseSchema = z.looseObject({
104
+ activityId: finiteNumber,
105
+ measurementCount: finiteNumber,
106
+ metricsCount: finiteNumber,
107
+ totalMetricsCount: finiteNumber,
108
+ metricDescriptors: z.array(GarminMetricDescriptorSchema),
109
+ activityDetailMetrics: z.array(GarminActivityDetailMetricSchema),
110
+ detailsAvailable: z.boolean(),
111
+ });
112
+ export const GarminWorkoutSourceSchema = z
113
+ .object({
114
+ activity: GarminApiActivitySchema,
115
+ details: GarminApiDetailsResponseSchema,
116
+ exerciseSets: GarminExerciseSetsResponseSchema,
117
+ })
118
+ .strict();
119
+ export const GarminCursorSchema = z
120
+ .object({
121
+ version: z.literal(1),
122
+ highestSyncedActivityId: finiteNumber,
123
+ })
124
+ .strict();
125
+ export const initialGarminCursor = {
126
+ version: 1,
127
+ highestSyncedActivityId: 0,
128
+ };
129
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/providers/garmin/types.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IGarminTokens } from \"garmin-connect/dist/garmin/types.js\";\nimport { z } from \"zod\";\n\nconst finiteNumber = z.number();\nconst optionalFiniteNumber = finiteNumber.nullish();\n\nexport type GarminTokens = IGarminTokens;\n\nexport const GarminTokensSchema = z\n .object({\n oauth1: z\n .object({\n oauth_token: z.string().min(1),\n oauth_token_secret: z.string().min(1),\n })\n .strict(),\n oauth2: z\n .object({\n scope: z.string().min(1),\n jti: z.string().min(1),\n access_token: z.string().min(1),\n token_type: z.string().min(1),\n refresh_token: z.string().min(1),\n expires_in: finiteNumber,\n refresh_token_expires_in: finiteNumber,\n expires_at: finiteNumber,\n refresh_token_expires_at: finiteNumber,\n last_update_date: z.string().min(1),\n expires_date: z.string().min(1),\n })\n .strict(),\n })\n .strict();\n\nexport const GarminApiActivityTypeSchema = z.looseObject({\n typeKey: z.string().min(1),\n});\n\nexport const GarminApiActivitySchema = z.looseObject({\n activityId: finiteNumber,\n activityName: z.string().min(1),\n startTimeGMT: z.string().min(1),\n activityType: GarminApiActivityTypeSchema,\n duration: optionalFiniteNumber,\n distance: optionalFiniteNumber,\n elevationGain: optionalFiniteNumber,\n elevationLoss: optionalFiniteNumber,\n startLatitude: optionalFiniteNumber,\n startLongitude: optionalFiniteNumber,\n calories: optionalFiniteNumber,\n averageHR: optionalFiniteNumber,\n maxHR: optionalFiniteNumber,\n averageRunningCadenceInStepsPerMinute: optionalFiniteNumber,\n avgStrideLength: optionalFiniteNumber,\n averageSpeed: optionalFiniteNumber,\n maxSpeed: optionalFiniteNumber,\n});\n\nexport const GarminApiActivitiesSchema = z.array(GarminApiActivitySchema);\n\nexport type GarminApiActivity = z.infer<typeof GarminApiActivitySchema>;\n\nexport const GarminApiExerciseSchema = z.looseObject({\n category: z.string().nullish(),\n name: z.string().nullish(),\n probability: optionalFiniteNumber,\n});\n\nexport type GarminApiExercise = z.infer<typeof GarminApiExerciseSchema>;\n\nexport const GarminApiExerciseSetSchema = z.looseObject({\n exercises: z.array(GarminApiExerciseSchema).nullish(),\n duration: optionalFiniteNumber,\n repetitionCount: optionalFiniteNumber,\n weight: optionalFiniteNumber,\n setType: z.string().nullish(),\n startTime: z.string().nullish(),\n wktStepIndex: optionalFiniteNumber,\n messageIndex: optionalFiniteNumber,\n});\n\nexport type GarminApiExerciseSet = z.infer<typeof GarminApiExerciseSetSchema>;\n\nexport const GarminApiExerciseSetsResponseSchema = z.looseObject({\n activityId: finiteNumber,\n exerciseSets: z.array(GarminApiExerciseSetSchema).nullable(),\n});\n\nexport type GarminApiExerciseSetsResponse = z.infer<\n typeof GarminApiExerciseSetsResponseSchema\n>;\n\nexport const GarminExerciseSchema = z.looseObject({\n category: z.string().min(1),\n name: z.string().min(1),\n probability: finiteNumber,\n});\n\nexport type GarminExercise = z.infer<typeof GarminExerciseSchema>;\n\nexport const GarminExerciseSetSchema = z.looseObject({\n exercises: z.array(GarminExerciseSchema),\n duration: optionalFiniteNumber,\n repetitionCount: optionalFiniteNumber,\n weight: optionalFiniteNumber,\n setType: z.string().nullish(),\n startTime: z.string().nullish(),\n wktStepIndex: optionalFiniteNumber,\n setIndex: finiteNumber,\n});\n\nexport type GarminExerciseSet = z.infer<typeof GarminExerciseSetSchema>;\n\nexport const GarminExerciseSetsResponseSchema = z.looseObject({\n activityId: finiteNumber,\n exerciseSets: z.array(GarminExerciseSetSchema),\n});\n\nexport type GarminExerciseSetsResponse = z.infer<\n typeof GarminExerciseSetsResponseSchema\n>;\n\nexport const GarminUnitSchema = z.looseObject({\n id: finiteNumber,\n key: z.string(),\n factor: finiteNumber,\n});\n\nexport const GarminMetricDescriptorSchema = z.looseObject({\n metricsIndex: finiteNumber,\n key: z.string(),\n unit: GarminUnitSchema,\n});\n\nexport const GarminActivityDetailMetricSchema = z.looseObject({\n metrics: z.array(finiteNumber.nullable()),\n});\n\nexport const GarminApiDetailsResponseSchema = z.looseObject({\n activityId: finiteNumber,\n measurementCount: finiteNumber,\n metricsCount: finiteNumber,\n totalMetricsCount: finiteNumber,\n metricDescriptors: z.array(GarminMetricDescriptorSchema),\n activityDetailMetrics: z.array(GarminActivityDetailMetricSchema),\n detailsAvailable: z.boolean(),\n});\n\nexport type GarminApiDetailsResponse = z.infer<\n typeof GarminApiDetailsResponseSchema\n>;\n\nexport const GarminWorkoutSourceSchema = z\n .object({\n activity: GarminApiActivitySchema,\n details: GarminApiDetailsResponseSchema,\n exerciseSets: GarminExerciseSetsResponseSchema,\n })\n .strict();\n\nexport type GarminWorkoutSource = z.infer<typeof GarminWorkoutSourceSchema>;\n\nexport const GarminCursorSchema = z\n .object({\n version: z.literal(1),\n highestSyncedActivityId: finiteNumber,\n })\n .strict();\n\nexport type GarminCursor = z.infer<typeof GarminCursorSchema>;\n\nexport const initialGarminCursor: GarminCursor = {\n version: 1,\n highestSyncedActivityId: 0,\n};\n"]}