@nativesquare/soma 0.9.4 → 0.10.1

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 (203) hide show
  1. package/dist/client/index.d.ts +124 -144
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +157 -134
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +18 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +113 -22
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/garmin/auth.d.ts +0 -4
  11. package/dist/component/garmin/auth.d.ts.map +1 -1
  12. package/dist/component/garmin/auth.js +0 -8
  13. package/dist/component/garmin/auth.js.map +1 -1
  14. package/dist/component/garmin/private.d.ts +10 -1
  15. package/dist/component/garmin/private.d.ts.map +1 -1
  16. package/dist/component/garmin/private.js +49 -9
  17. package/dist/component/garmin/private.js.map +1 -1
  18. package/dist/component/garmin/public.d.ts +237 -62
  19. package/dist/component/garmin/public.d.ts.map +1 -1
  20. package/dist/component/garmin/public.js +689 -254
  21. package/dist/component/garmin/public.js.map +1 -1
  22. package/dist/component/garmin/utils.d.ts +8 -0
  23. package/dist/component/garmin/utils.d.ts.map +1 -1
  24. package/dist/component/garmin/utils.js +9 -0
  25. package/dist/component/garmin/utils.js.map +1 -1
  26. package/dist/component/schema.d.ts +2 -2
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +5 -3
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/{strava → component/strava}/auth.d.ts +15 -48
  31. package/dist/component/strava/auth.d.ts.map +1 -0
  32. package/dist/{strava → component/strava}/auth.js +4 -39
  33. package/dist/component/strava/auth.js.map +1 -0
  34. package/dist/component/strava/client.d.ts +8 -0
  35. package/dist/component/strava/client.d.ts.map +1 -0
  36. package/dist/component/strava/client.js +18 -0
  37. package/dist/component/strava/client.js.map +1 -0
  38. package/dist/component/strava/private.d.ts +19 -0
  39. package/dist/component/strava/private.d.ts.map +1 -1
  40. package/dist/component/strava/private.js +52 -2
  41. package/dist/component/strava/private.js.map +1 -1
  42. package/dist/component/strava/public.d.ts +54 -19
  43. package/dist/component/strava/public.d.ts.map +1 -1
  44. package/dist/component/strava/public.js +159 -109
  45. package/dist/component/strava/public.js.map +1 -1
  46. package/dist/component/strava/transform/activity.d.ts +19 -0
  47. package/dist/component/strava/transform/activity.d.ts.map +1 -0
  48. package/dist/{strava → component/strava/transform}/activity.js +31 -45
  49. package/dist/component/strava/transform/activity.js.map +1 -0
  50. package/dist/{strava → component/strava/transform}/athlete.d.ts +4 -10
  51. package/dist/component/strava/transform/athlete.d.ts.map +1 -0
  52. package/dist/{strava → component/strava/transform}/athlete.js +2 -8
  53. package/dist/component/strava/transform/athlete.js.map +1 -0
  54. package/dist/component/strava/transform/maps/sportType.d.ts +7 -0
  55. package/dist/component/strava/transform/maps/sportType.d.ts.map +1 -0
  56. package/dist/{strava/maps/sport-type.js → component/strava/transform/maps/sportType.js} +4 -2
  57. package/dist/component/strava/transform/maps/sportType.js.map +1 -0
  58. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts +3 -0
  59. package/dist/component/strava/types/stravaApi/client/client.gen.d.ts.map +1 -0
  60. package/dist/component/strava/types/stravaApi/client/client.gen.js +236 -0
  61. package/dist/component/strava/types/stravaApi/client/client.gen.js.map +1 -0
  62. package/dist/component/strava/types/stravaApi/client/index.d.ts +9 -0
  63. package/dist/component/strava/types/stravaApi/client/index.d.ts.map +1 -0
  64. package/dist/component/strava/types/stravaApi/client/index.js +7 -0
  65. package/dist/component/strava/types/stravaApi/client/index.js.map +1 -0
  66. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts +118 -0
  67. package/dist/component/strava/types/stravaApi/client/types.gen.d.ts.map +1 -0
  68. package/dist/component/strava/types/stravaApi/client/types.gen.js +3 -0
  69. package/dist/component/strava/types/stravaApi/client/types.gen.js.map +1 -0
  70. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts +34 -0
  71. package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts.map +1 -0
  72. package/dist/component/strava/types/stravaApi/client/utils.gen.js +229 -0
  73. package/dist/component/strava/types/stravaApi/client/utils.gen.js.map +1 -0
  74. package/dist/component/strava/types/stravaApi/client.gen.d.ts +13 -0
  75. package/dist/component/strava/types/stravaApi/client.gen.d.ts.map +1 -0
  76. package/dist/component/strava/types/stravaApi/client.gen.js +4 -0
  77. package/dist/component/strava/types/stravaApi/client.gen.js.map +1 -0
  78. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts +19 -0
  79. package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts.map +1 -0
  80. package/dist/component/strava/types/stravaApi/core/auth.gen.js +15 -0
  81. package/dist/component/strava/types/stravaApi/core/auth.gen.js.map +1 -0
  82. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts +26 -0
  83. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts.map +1 -0
  84. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js +58 -0
  85. package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js.map +1 -0
  86. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts +44 -0
  87. package/dist/component/strava/types/stravaApi/core/params.gen.d.ts.map +1 -0
  88. package/dist/component/strava/types/stravaApi/core/params.gen.js +101 -0
  89. package/dist/component/strava/types/stravaApi/core/params.gen.js.map +1 -0
  90. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts +34 -0
  91. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts.map +1 -0
  92. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js +107 -0
  93. package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js.map +1 -0
  94. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts +19 -0
  95. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts.map +1 -0
  96. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js +93 -0
  97. package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js.map +1 -0
  98. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts +72 -0
  99. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts.map +1 -0
  100. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js +134 -0
  101. package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js.map +1 -0
  102. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts +79 -0
  103. package/dist/component/strava/types/stravaApi/core/types.gen.d.ts.map +1 -0
  104. package/dist/component/strava/types/stravaApi/core/types.gen.js +3 -0
  105. package/dist/component/strava/types/stravaApi/core/types.gen.js.map +1 -0
  106. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts +20 -0
  107. package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts.map +1 -0
  108. package/dist/component/strava/types/stravaApi/core/utils.gen.js +88 -0
  109. package/dist/component/strava/types/stravaApi/core/utils.gen.js.map +1 -0
  110. package/dist/component/strava/types/stravaApi/index.d.ts +3 -0
  111. package/dist/component/strava/types/stravaApi/index.d.ts.map +1 -0
  112. package/dist/component/strava/types/stravaApi/index.js +3 -0
  113. package/dist/component/strava/types/stravaApi/index.js.map +1 -0
  114. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts +224 -0
  115. package/dist/component/strava/types/stravaApi/sdk.gen.d.ts.map +1 -0
  116. package/dist/component/strava/types/stravaApi/sdk.gen.js +361 -0
  117. package/dist/component/strava/types/stravaApi/sdk.gen.js.map +1 -0
  118. package/dist/component/strava/types/stravaApi/types.gen.d.ts +2209 -0
  119. package/dist/component/strava/types/stravaApi/types.gen.d.ts.map +1 -0
  120. package/dist/component/strava/types/stravaApi/types.gen.js +3 -0
  121. package/dist/component/strava/types/stravaApi/types.gen.js.map +1 -0
  122. package/dist/component/strava/types/stravaApi/zod.gen.d.ts +5332 -0
  123. package/dist/component/strava/types/stravaApi/zod.gen.d.ts.map +1 -0
  124. package/dist/component/strava/types/stravaApi/zod.gen.js +1009 -0
  125. package/dist/component/strava/types/stravaApi/zod.gen.js.map +1 -0
  126. package/dist/component/strava/utils.d.ts +15 -0
  127. package/dist/component/strava/utils.d.ts.map +1 -0
  128. package/dist/component/strava/utils.js +36 -0
  129. package/dist/component/strava/utils.js.map +1 -0
  130. package/dist/component/utils.d.ts +5 -0
  131. package/dist/component/utils.d.ts.map +1 -0
  132. package/dist/component/utils.js +11 -0
  133. package/dist/component/utils.js.map +1 -0
  134. package/package.json +131 -130
  135. package/src/client/index.ts +285 -164
  136. package/src/component/_generated/api.ts +18 -0
  137. package/src/component/_generated/component.ts +191 -24
  138. package/src/component/garmin/auth.ts +0 -9
  139. package/src/component/garmin/private.ts +84 -12
  140. package/src/component/garmin/public.ts +812 -348
  141. package/src/component/garmin/utils.ts +17 -0
  142. package/src/component/schema.ts +5 -3
  143. package/src/{strava → component/strava}/auth.ts +143 -185
  144. package/src/component/strava/client.ts +20 -0
  145. package/src/component/strava/private.ts +147 -89
  146. package/src/component/strava/public.ts +191 -139
  147. package/src/{strava → component/strava/transform}/activity.ts +256 -276
  148. package/src/{strava → component/strava/transform}/athlete.ts +41 -47
  149. package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
  150. package/src/component/strava/types/specs/strava-api.json +4796 -0
  151. package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
  152. package/src/component/strava/types/stravaApi/client/index.ts +25 -0
  153. package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
  154. package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
  155. package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
  156. package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
  157. package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
  158. package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
  159. package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
  160. package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
  161. package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
  162. package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
  163. package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
  164. package/src/component/strava/types/stravaApi/index.ts +4 -0
  165. package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
  166. package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
  167. package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
  168. package/src/component/strava/utils.ts +52 -0
  169. package/src/component/utils.ts +11 -0
  170. package/dist/strava/activity.d.ts +0 -121
  171. package/dist/strava/activity.d.ts.map +0 -1
  172. package/dist/strava/activity.js.map +0 -1
  173. package/dist/strava/athlete.d.ts.map +0 -1
  174. package/dist/strava/athlete.js.map +0 -1
  175. package/dist/strava/auth.d.ts.map +0 -1
  176. package/dist/strava/auth.js.map +0 -1
  177. package/dist/strava/client.d.ts +0 -93
  178. package/dist/strava/client.d.ts.map +0 -1
  179. package/dist/strava/client.js +0 -158
  180. package/dist/strava/client.js.map +0 -1
  181. package/dist/strava/index.d.ts +0 -13
  182. package/dist/strava/index.d.ts.map +0 -1
  183. package/dist/strava/index.js +0 -17
  184. package/dist/strava/index.js.map +0 -1
  185. package/dist/strava/maps/sport-type.d.ts +0 -7
  186. package/dist/strava/maps/sport-type.d.ts.map +0 -1
  187. package/dist/strava/maps/sport-type.js.map +0 -1
  188. package/dist/strava/sync.d.ts +0 -104
  189. package/dist/strava/sync.d.ts.map +0 -1
  190. package/dist/strava/sync.js +0 -87
  191. package/dist/strava/sync.js.map +0 -1
  192. package/dist/strava/types.d.ts +0 -266
  193. package/dist/strava/types.d.ts.map +0 -1
  194. package/dist/strava/types.js +0 -8
  195. package/dist/strava/types.js.map +0 -1
  196. package/src/strava/activity.test.ts +0 -415
  197. package/src/strava/athlete.test.ts +0 -139
  198. package/src/strava/auth.test.ts +0 -78
  199. package/src/strava/client.ts +0 -212
  200. package/src/strava/index.ts +0 -54
  201. package/src/strava/maps/sport-type.test.ts +0 -69
  202. package/src/strava/sync.ts +0 -168
  203. package/src/strava/types.ts +0 -361
@@ -7,61 +7,120 @@ import { v } from "convex/values";
7
7
  import { action } from "../_generated/server";
8
8
  import { api, internal } from "../_generated/api";
9
9
  import type { Doc, Id } from "../_generated/dataModel";
10
- import { StravaClient } from "../../strava/client";
10
+ import { createStravaClient } from "./client.js";
11
+ import { listAllActivities } from "./utils.js";
11
12
  import {
13
+ getLoggedInAthlete,
14
+ getActivityById,
15
+ getActivityStreams,
16
+ } from "./types/stravaApi/sdk.gen.js";
17
+ import { generateState } from "../utils.js";
18
+ import {
19
+ buildAuthUrl,
12
20
  exchangeCode,
13
21
  refreshToken as refreshStravaToken,
14
- } from "../../strava/auth";
15
- import { transformActivity } from "../../strava/activity";
16
- import { transformAthlete } from "../../strava/athlete";
22
+ } from "./auth.js";
23
+ import { transformActivity } from "./transform/activity.js";
24
+ import { transformAthlete } from "./transform/athlete.js";
17
25
 
18
26
  // ─── Public Actions ──────────────────────────────────────────────────────────
19
27
 
20
28
  /**
21
- * Full Strava OAuth callback handler.
22
- *
23
- * Exchanges the authorization code for tokens, creates/reactivates the
24
- * Soma connection, stores tokens securely, syncs the athlete profile,
25
- * and syncs all activities.
29
+ * Generate a Strava OAuth authorization URL.
26
30
  *
27
- * Returns `{ connectionId, synced, errors }`.
31
+ * The state parameter is stored in the component's `pendingOAuth` table
32
+ * so that `completeStravaOAuth` can look it up automatically when the
33
+ * callback fires via `registerRoutes`.
28
34
  */
29
- export const connectStrava = action({
35
+ export const getStravaAuthUrl = action({
30
36
  args: {
37
+ clientId: v.string(),
38
+ redirectUri: v.string(),
39
+ scope: v.optional(v.string()),
31
40
  userId: v.string(),
41
+ },
42
+ handler: async (ctx, args) => {
43
+ const state = generateState();
44
+
45
+ const authUrl = buildAuthUrl({
46
+ clientId: args.clientId,
47
+ redirectUri: args.redirectUri,
48
+ scope: args.scope,
49
+ state,
50
+ });
51
+
52
+ await ctx.runMutation(internal.strava.private.storePendingOAuth, {
53
+ provider: "STRAVA",
54
+ state,
55
+ userId: args.userId,
56
+ });
57
+
58
+ return { authUrl, state };
59
+ },
60
+ });
61
+
62
+ /**
63
+ * Complete a Strava OAuth flow using stored pending state.
64
+ *
65
+ * Called internally by `registerRoutes` — the callback handler calls
66
+ * this with the `code` and `state` from the redirect. The action looks
67
+ * up the pending state (userId) stored during `getStravaAuthUrl`,
68
+ * exchanges for tokens, creates the connection, stores tokens, and
69
+ * cleans up the pending entry.
70
+ *
71
+ * The host app is responsible for calling `syncStrava` afterwards.
72
+ */
73
+ export const completeStravaOAuth = action({
74
+ args: {
32
75
  code: v.string(),
76
+ state: v.string(),
33
77
  clientId: v.string(),
34
78
  clientSecret: v.string(),
35
- baseUrl: v.optional(v.string()),
36
- includeStreams: v.optional(v.boolean()),
37
79
  },
38
80
  returns: v.object({
39
81
  connectionId: v.string(),
40
- synced: v.number(),
41
- errors: v.array(
42
- v.object({ activityId: v.number(), error: v.string() }),
43
- ),
82
+ userId: v.string(),
44
83
  }),
45
- handler: async (ctx, args) => {
46
- // 1. Exchange authorization code for tokens
84
+ handler: async (ctx, args): Promise<{
85
+ connectionId: Id<"connections">;
86
+ userId: string;
87
+ }> => {
88
+ // 1. Look up pending state
89
+ const pending: Doc<"pendingOAuth"> | null = await ctx.runQuery(
90
+ internal.strava.private.getPendingOAuth,
91
+ { state: args.state },
92
+ );
93
+ if (!pending) {
94
+ throw new Error(
95
+ "No pending Strava OAuth state found for this state parameter. " +
96
+ "The authorization may have expired or was already used.",
97
+ );
98
+ }
99
+
100
+ // 2. Exchange authorization code for tokens
47
101
  const tokens = await exchangeCode({
48
102
  clientId: args.clientId,
49
103
  clientSecret: args.clientSecret,
50
104
  code: args.code,
51
- baseUrl: args.baseUrl,
52
105
  });
53
106
 
54
- // 2. Create/reactivate the Soma connection
107
+ // 3. Clean up pending entry
108
+ await ctx.runMutation(
109
+ internal.strava.private.deletePendingOAuth,
110
+ { state: args.state },
111
+ );
112
+
113
+ // 4. Create/reactivate the Soma connection
55
114
  const connectionId: Id<"connections"> = await ctx.runMutation(
56
115
  api.public.connect,
57
116
  {
58
- userId: args.userId,
117
+ userId: pending.userId,
59
118
  provider: "STRAVA",
60
119
  },
61
120
  );
62
121
 
63
- // 3. Store OAuth tokens in providerTokens table
64
- const _stored: null = await ctx.runMutation(
122
+ // 5. Store OAuth tokens
123
+ await ctx.runMutation(
65
124
  internal.strava.private.storeTokens,
66
125
  {
67
126
  connectionId,
@@ -71,62 +130,10 @@ export const connectStrava = action({
71
130
  },
72
131
  );
73
132
 
74
- // 4. Sync athlete profile
75
- const client = new StravaClient({
76
- accessToken: tokens.access_token,
77
- baseUrl: args.baseUrl,
78
- });
79
-
80
- const athlete = await client.getAthlete();
81
- const athleteData = transformAthlete(athlete);
82
- const _athleteId: Id<"athletes"> = await ctx.runMutation(
83
- api.public.ingestAthlete,
84
- {
85
- connectionId,
86
- userId: args.userId,
87
- ...athleteData,
88
- } as never,
89
- );
90
-
91
- // 5. Sync all activities
92
- const summaries = await client.listAllActivities();
93
- let synced = 0;
94
- const errors: Array<{ activityId: number; error: string }> = [];
95
-
96
- for (const summary of summaries) {
97
- try {
98
- const detailed = await client.getActivity(summary.id);
99
- const streams = args.includeStreams
100
- ? await client.getActivityStreams(summary.id)
101
- : undefined;
102
- const data = transformActivity(detailed, { streams });
103
- const _activityId: Id<"activities"> = await ctx.runMutation(
104
- api.public.ingestActivity,
105
- {
106
- connectionId,
107
- userId: args.userId,
108
- ...data,
109
- } as never,
110
- );
111
- synced++;
112
- } catch (err) {
113
- errors.push({
114
- activityId: summary.id,
115
- error: err instanceof Error ? err.message : String(err),
116
- });
117
- }
118
- }
119
-
120
- // 6. Update lastDataUpdate timestamp
121
- const _updated: null = await ctx.runMutation(
122
- api.public.updateConnection,
123
- {
124
- connectionId,
125
- lastDataUpdate: new Date().toISOString(),
126
- },
127
- );
128
-
129
- return { connectionId, synced, errors };
133
+ return {
134
+ connectionId,
135
+ userId: pending.userId,
136
+ };
130
137
  },
131
138
  });
132
139
 
@@ -143,17 +150,18 @@ export const syncStrava = action({
143
150
  userId: v.string(),
144
151
  clientId: v.string(),
145
152
  clientSecret: v.string(),
146
- baseUrl: v.optional(v.string()),
147
- includeStreams: v.optional(v.boolean()),
148
153
  after: v.optional(v.number()),
149
154
  },
150
155
  returns: v.object({
151
- synced: v.number(),
156
+ synced: v.object({ athletes: v.number(), activities: v.number() }),
152
157
  errors: v.array(
153
- v.object({ activityId: v.number(), error: v.string() }),
158
+ v.object({ type: v.string(), id: v.string(), error: v.string() }),
154
159
  ),
155
160
  }),
156
- handler: async (ctx, args) => {
161
+ handler: async (ctx, args): Promise<{
162
+ synced: { athletes: number; activities: number };
163
+ errors: Array<{ type: string; id: string; error: string }>;
164
+ }> => {
157
165
  // 1. Look up connection
158
166
  const connection: Doc<"connections"> | null = await ctx.runQuery(
159
167
  internal.private.getConnectionByProvider,
@@ -162,7 +170,7 @@ export const syncStrava = action({
162
170
  if (!connection) {
163
171
  throw new Error(
164
172
  `No Strava connection found for user "${args.userId}". ` +
165
- "Call connectStrava first.",
173
+ "Connect to Strava first via getStravaAuthUrl.",
166
174
  );
167
175
  }
168
176
  if (!connection.active) {
@@ -199,10 +207,9 @@ export const syncStrava = action({
199
207
  clientId: args.clientId,
200
208
  clientSecret: args.clientSecret,
201
209
  refreshToken: tokenDoc.refreshToken,
202
- baseUrl: args.baseUrl,
203
210
  });
204
211
  accessToken = refreshed.access_token;
205
- const _refreshed: null = await ctx.runMutation(
212
+ await ctx.runMutation(
206
213
  internal.strava.private.storeTokens,
207
214
  {
208
215
  connectionId,
@@ -213,68 +220,118 @@ export const syncStrava = action({
213
220
  );
214
221
  }
215
222
 
216
- // 4. Create client and sync
217
- const client = new StravaClient({
223
+ // 4. Sync all data types
224
+ const result = await ctx.runAction(api.strava.public.syncAllTypes, {
218
225
  accessToken,
219
- baseUrl: args.baseUrl,
226
+ connectionId,
227
+ userId: args.userId,
228
+ after: args.after,
220
229
  });
221
230
 
222
- // Sync athlete profile
223
- const athlete = await client.getAthlete();
224
- const athleteData = transformAthlete(athlete);
225
- const _athleteId: Id<"athletes"> = await ctx.runMutation(
226
- api.public.ingestAthlete,
231
+ // 5. Update lastDataUpdate timestamp
232
+ await ctx.runMutation(
233
+ api.public.updateConnection,
227
234
  {
228
235
  connectionId,
229
- userId: args.userId,
230
- ...athleteData,
231
- } as never,
236
+ lastDataUpdate: new Date().toISOString(),
237
+ },
232
238
  );
233
239
 
234
- // Sync activities (optionally incremental via `after`)
235
- const summaries = await client.listAllActivities({
236
- after: args.after,
237
- });
238
- let synced = 0;
239
- const errors: Array<{ activityId: number; error: string }> = [];
240
+ return { synced: result.synced, errors: result.errors };
241
+ },
242
+ });
240
243
 
241
- for (const summary of summaries) {
242
- try {
243
- const detailed = await client.getActivity(summary.id);
244
- const streams = args.includeStreams
245
- ? await client.getActivityStreams(summary.id)
246
- : undefined;
247
- const data = transformActivity(detailed, { streams });
248
- const _activityId: Id<"activities"> = await ctx.runMutation(
249
- api.public.ingestActivity,
250
- {
244
+ // ─── Sync Engine ────────────────────────────────────────────────────────────
245
+
246
+ /**
247
+ * Fetch and ingest all Strava data types for a connected user.
248
+ *
249
+ * Called by syncStrava after obtaining a valid access token.
250
+ */
251
+ export const syncAllTypes = action({
252
+ args: {
253
+ accessToken: v.string(),
254
+ connectionId: v.id("connections"),
255
+ userId: v.string(),
256
+ after: v.optional(v.number()),
257
+ before: v.optional(v.number()),
258
+ },
259
+ handler: async (ctx, args) => {
260
+ const { accessToken, connectionId, userId } = args;
261
+ const client = createStravaClient(accessToken);
262
+
263
+ const synced = { athletes: 0, activities: 0 };
264
+ const errors: Array<{ type: string; id: string; error: string }> = [];
265
+
266
+ // ── Athlete ──────────────────────────────────────────────────────────
267
+ try {
268
+ const { data: athlete, error } = await getLoggedInAthlete({ client });
269
+ if (error || !athlete) throw new Error(error ? JSON.stringify(error) : "No athlete data");
270
+ const data = transformAthlete(athlete);
271
+ await ctx.runMutation(api.public.ingestAthlete, {
272
+ connectionId,
273
+ userId,
274
+ ...data,
275
+ } as never);
276
+ synced.athletes++;
277
+ } catch (err) {
278
+ errors.push({
279
+ type: "athlete",
280
+ id: "fetch",
281
+ error: err instanceof Error ? err.message : String(err),
282
+ });
283
+ }
284
+
285
+ // ── Activities ───────────────────────────────────────────────────────
286
+ try {
287
+ const summaries = await listAllActivities(client, {
288
+ after: args.after,
289
+ before: args.before,
290
+ });
291
+ for (const summary of summaries) {
292
+ if (summary.id == null) continue;
293
+ try {
294
+ const { data: detailed, error: detailError } = await getActivityById({ client, path: { id: summary.id } });
295
+ if (detailError || !detailed) throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
296
+
297
+ const { data: streams } = await getActivityStreams({
298
+ client,
299
+ path: { id: summary.id },
300
+ query: {
301
+ keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
302
+ key_by_type: true,
303
+ },
304
+ });
305
+
306
+ const data = transformActivity(detailed, { streams: streams ?? undefined });
307
+ await ctx.runMutation(api.public.ingestActivity, {
251
308
  connectionId,
252
- userId: args.userId,
309
+ userId,
253
310
  ...data,
254
- } as never,
255
- );
256
- synced++;
257
- } catch (err) {
258
- errors.push({
259
- activityId: summary.id,
260
- error: err instanceof Error ? err.message : String(err),
261
- });
311
+ } as never);
312
+ synced.activities++;
313
+ } catch (err) {
314
+ errors.push({
315
+ type: "activity",
316
+ id: String(summary.id),
317
+ error: err instanceof Error ? err.message : String(err),
318
+ });
319
+ }
262
320
  }
321
+ } catch (err) {
322
+ errors.push({
323
+ type: "activity",
324
+ id: "fetch",
325
+ error: err instanceof Error ? err.message : String(err),
326
+ });
263
327
  }
264
328
 
265
- // 5. Update lastDataUpdate timestamp
266
- const _updated: null = await ctx.runMutation(
267
- api.public.updateConnection,
268
- {
269
- connectionId,
270
- lastDataUpdate: new Date().toISOString(),
271
- },
272
- );
273
-
274
329
  return { synced, errors };
275
330
  },
276
331
  });
277
332
 
333
+ // ─── Disconnect ─────────────────────────────────────────────────────────────
334
+
278
335
  /**
279
336
  * Disconnect a user from Strava.
280
337
  *
@@ -286,7 +343,6 @@ export const disconnectStrava = action({
286
343
  userId: v.string(),
287
344
  clientId: v.string(),
288
345
  clientSecret: v.string(),
289
- baseUrl: v.optional(v.string()),
290
346
  },
291
347
  returns: v.null(),
292
348
  handler: async (ctx, args) => {
@@ -310,11 +366,7 @@ export const disconnectStrava = action({
310
366
  );
311
367
  if (tokenDoc) {
312
368
  try {
313
- const base = (args.baseUrl ?? "https://www.strava.com").replace(
314
- /\/+$/,
315
- "",
316
- );
317
- await fetch(`${base}/oauth/deauthorize`, {
369
+ await fetch("https://www.strava.com/oauth/deauthorize", {
318
370
  method: "POST",
319
371
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
320
372
  body: `access_token=${tokenDoc.accessToken}`,