@nativesquare/soma 0.9.3 → 0.10.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 +96 -33
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +80 -35
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +18 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +43 -9
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/auth.d.ts +0 -4
- package/dist/component/garmin/auth.d.ts.map +1 -1
- package/dist/component/garmin/auth.js +0 -8
- package/dist/component/garmin/auth.js.map +1 -1
- package/dist/component/garmin/private.d.ts +20 -3
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +17 -26
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +4 -4
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +6 -1
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +4 -0
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +23 -18
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +5 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/{strava → component/strava}/auth.d.ts +15 -48
- package/dist/component/strava/auth.d.ts.map +1 -0
- package/dist/{strava → component/strava}/auth.js +4 -39
- package/dist/component/strava/auth.js.map +1 -0
- package/dist/component/strava/client.d.ts +8 -0
- package/dist/component/strava/client.d.ts.map +1 -0
- package/dist/component/strava/client.js +18 -0
- package/dist/component/strava/client.js.map +1 -0
- package/dist/component/strava/private.d.ts +19 -0
- package/dist/component/strava/private.d.ts.map +1 -1
- package/dist/component/strava/private.js +52 -2
- package/dist/component/strava/private.js.map +1 -1
- package/dist/component/strava/public.d.ts +87 -12
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +218 -92
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/transform/activity.d.ts +19 -0
- package/dist/component/strava/transform/activity.d.ts.map +1 -0
- package/dist/{strava → component/strava/transform}/activity.js +21 -41
- package/dist/component/strava/transform/activity.js.map +1 -0
- package/dist/{strava → component/strava/transform}/athlete.d.ts +4 -10
- package/dist/component/strava/transform/athlete.d.ts.map +1 -0
- package/dist/{strava → component/strava/transform}/athlete.js +2 -8
- package/dist/component/strava/transform/athlete.js.map +1 -0
- package/dist/component/strava/transform/maps/sportType.d.ts +7 -0
- package/dist/component/strava/transform/maps/sportType.d.ts.map +1 -0
- package/dist/{strava/maps/sport-type.js → component/strava/transform/maps/sportType.js} +4 -2
- package/dist/component/strava/transform/maps/sportType.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.d.ts +3 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.js +236 -0
- package/dist/component/strava/types/stravaApi/client/client.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/index.d.ts +9 -0
- package/dist/component/strava/types/stravaApi/client/index.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/index.js +7 -0
- package/dist/component/strava/types/stravaApi/client/index.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.d.ts +118 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/client/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts +34 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.js +229 -0
- package/dist/component/strava/types/stravaApi/client/utils.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/client.gen.d.ts +13 -0
- package/dist/component/strava/types/stravaApi/client.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/client.gen.js +4 -0
- package/dist/component/strava/types/stravaApi/client.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts +19 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.js +15 -0
- package/dist/component/strava/types/stravaApi/core/auth.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts +26 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js +58 -0
- package/dist/component/strava/types/stravaApi/core/bodySerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.d.ts +44 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.js +101 -0
- package/dist/component/strava/types/stravaApi/core/params.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts +34 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js +107 -0
- package/dist/component/strava/types/stravaApi/core/pathSerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts +19 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js +93 -0
- package/dist/component/strava/types/stravaApi/core/queryKeySerializer.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts +72 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js +134 -0
- package/dist/component/strava/types/stravaApi/core/serverSentEvents.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.d.ts +79 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/core/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts +20 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.js +88 -0
- package/dist/component/strava/types/stravaApi/core/utils.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/index.d.ts +3 -0
- package/dist/component/strava/types/stravaApi/index.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/index.js +3 -0
- package/dist/component/strava/types/stravaApi/index.js.map +1 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.d.ts +224 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.js +361 -0
- package/dist/component/strava/types/stravaApi/sdk.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/types.gen.d.ts +2209 -0
- package/dist/component/strava/types/stravaApi/types.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/types.gen.js +3 -0
- package/dist/component/strava/types/stravaApi/types.gen.js.map +1 -0
- package/dist/component/strava/types/stravaApi/zod.gen.d.ts +5332 -0
- package/dist/component/strava/types/stravaApi/zod.gen.d.ts.map +1 -0
- package/dist/component/strava/types/stravaApi/zod.gen.js +1009 -0
- package/dist/component/strava/types/stravaApi/zod.gen.js.map +1 -0
- package/dist/component/strava/utils.d.ts +15 -0
- package/dist/component/strava/utils.d.ts.map +1 -0
- package/dist/component/strava/utils.js +36 -0
- package/dist/component/strava/utils.js.map +1 -0
- package/dist/component/utils.d.ts +5 -0
- package/dist/component/utils.d.ts.map +1 -0
- package/dist/component/utils.js +11 -0
- package/dist/component/utils.js.map +1 -0
- package/package.json +131 -130
- package/src/client/index.ts +121 -52
- package/src/component/_generated/api.ts +18 -0
- package/src/component/_generated/component.ts +44 -11
- package/src/component/garmin/auth.ts +0 -9
- package/src/component/garmin/private.ts +0 -12
- package/src/component/garmin/public.ts +8 -1
- package/src/component/schema.ts +5 -3
- package/src/{strava → component/strava}/auth.ts +143 -185
- package/src/component/strava/client.ts +20 -0
- package/src/component/strava/private.ts +147 -89
- package/src/component/strava/public.ts +268 -110
- package/src/{strava → component/strava/transform}/activity.ts +256 -276
- package/src/{strava → component/strava/transform}/athlete.ts +41 -47
- package/src/{strava/maps/sport-type.ts → component/strava/transform/maps/sportType.ts} +100 -99
- package/src/component/strava/types/specs/strava-api.json +4796 -0
- package/src/component/strava/types/stravaApi/client/client.gen.ts +290 -0
- package/src/component/strava/types/stravaApi/client/index.ts +25 -0
- package/src/component/strava/types/stravaApi/client/types.gen.ts +214 -0
- package/src/component/strava/types/stravaApi/client/utils.gen.ts +316 -0
- package/src/component/strava/types/stravaApi/client.gen.ts +16 -0
- package/src/component/strava/types/stravaApi/core/auth.gen.ts +41 -0
- package/src/component/strava/types/stravaApi/core/bodySerializer.gen.ts +82 -0
- package/src/component/strava/types/stravaApi/core/params.gen.ts +169 -0
- package/src/component/strava/types/stravaApi/core/pathSerializer.gen.ts +171 -0
- package/src/component/strava/types/stravaApi/core/queryKeySerializer.gen.ts +117 -0
- package/src/component/strava/types/stravaApi/core/serverSentEvents.gen.ts +243 -0
- package/src/component/strava/types/stravaApi/core/types.gen.ts +104 -0
- package/src/component/strava/types/stravaApi/core/utils.gen.ts +140 -0
- package/src/component/strava/types/stravaApi/index.ts +4 -0
- package/src/component/strava/types/stravaApi/sdk.gen.ts +410 -0
- package/src/component/strava/types/stravaApi/types.gen.ts +2435 -0
- package/src/component/strava/types/stravaApi/zod.gen.ts +1132 -0
- package/src/component/strava/utils.ts +52 -0
- package/src/component/utils.ts +11 -0
- package/dist/strava/activity.d.ts +0 -121
- package/dist/strava/activity.d.ts.map +0 -1
- package/dist/strava/activity.js.map +0 -1
- package/dist/strava/athlete.d.ts.map +0 -1
- package/dist/strava/athlete.js.map +0 -1
- package/dist/strava/auth.d.ts.map +0 -1
- package/dist/strava/auth.js.map +0 -1
- package/dist/strava/client.d.ts +0 -93
- package/dist/strava/client.d.ts.map +0 -1
- package/dist/strava/client.js +0 -158
- package/dist/strava/client.js.map +0 -1
- package/dist/strava/index.d.ts +0 -13
- package/dist/strava/index.d.ts.map +0 -1
- package/dist/strava/index.js +0 -17
- package/dist/strava/index.js.map +0 -1
- package/dist/strava/maps/sport-type.d.ts +0 -7
- package/dist/strava/maps/sport-type.d.ts.map +0 -1
- package/dist/strava/maps/sport-type.js.map +0 -1
- package/dist/strava/sync.d.ts +0 -104
- package/dist/strava/sync.d.ts.map +0 -1
- package/dist/strava/sync.js +0 -87
- package/dist/strava/sync.js.map +0 -1
- package/dist/strava/types.d.ts +0 -266
- package/dist/strava/types.d.ts.map +0 -1
- package/dist/strava/types.js +0 -8
- package/dist/strava/types.js.map +0 -1
- package/src/strava/activity.test.ts +0 -415
- package/src/strava/athlete.test.ts +0 -139
- package/src/strava/auth.test.ts +0 -78
- package/src/strava/client.ts +0 -212
- package/src/strava/index.ts +0 -54
- package/src/strava/maps/sport-type.test.ts +0 -69
- package/src/strava/sync.ts +0 -168
- package/src/strava/types.ts +0 -361
|
@@ -7,48 +7,100 @@ 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 {
|
|
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 "
|
|
15
|
-
import { transformActivity } from "
|
|
16
|
-
import { transformAthlete } from "
|
|
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
|
-
*
|
|
29
|
+
* Generate a Strava OAuth authorization URL.
|
|
30
|
+
*
|
|
31
|
+
* If `userId` is provided, the state parameter is stored in the component's
|
|
32
|
+
* `pendingOAuth` table so that `completeStravaOAuth` can look it up
|
|
33
|
+
* automatically when the callback fires. This is the recommended flow
|
|
34
|
+
* when using `registerRoutes`.
|
|
35
|
+
*
|
|
36
|
+
* If `userId` is omitted, the host app must store the returned `state`
|
|
37
|
+
* itself and pass the userId to `connectStrava` manually.
|
|
38
|
+
*/
|
|
39
|
+
export const getStravaAuthUrl = action({
|
|
40
|
+
args: {
|
|
41
|
+
clientId: v.string(),
|
|
42
|
+
redirectUri: v.string(),
|
|
43
|
+
scope: v.optional(v.string()),
|
|
44
|
+
userId: v.optional(v.string()),
|
|
45
|
+
},
|
|
46
|
+
handler: async (ctx, args) => {
|
|
47
|
+
const state = generateState();
|
|
48
|
+
|
|
49
|
+
const authUrl = buildAuthUrl({
|
|
50
|
+
clientId: args.clientId,
|
|
51
|
+
redirectUri: args.redirectUri,
|
|
52
|
+
scope: args.scope,
|
|
53
|
+
state,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (args.userId) {
|
|
57
|
+
await ctx.runMutation(internal.strava.private.storePendingOAuth, {
|
|
58
|
+
provider: "STRAVA",
|
|
59
|
+
state,
|
|
60
|
+
userId: args.userId,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { authUrl, state };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Full Strava OAuth callback handler (manual flow).
|
|
22
70
|
*
|
|
23
71
|
* Exchanges the authorization code for tokens, creates/reactivates the
|
|
24
72
|
* Soma connection, stores tokens securely, syncs the athlete profile,
|
|
25
73
|
* and syncs all activities.
|
|
26
74
|
*
|
|
75
|
+
* Used when the host app handles the callback itself and passes the
|
|
76
|
+
* userId directly (analogous to `connectGarmin`).
|
|
77
|
+
*
|
|
27
78
|
* Returns `{ connectionId, synced, errors }`.
|
|
28
79
|
*/
|
|
29
80
|
export const connectStrava = action({
|
|
30
81
|
args: {
|
|
31
82
|
userId: v.string(),
|
|
32
|
-
code: v.string(),
|
|
33
83
|
clientId: v.string(),
|
|
34
84
|
clientSecret: v.string(),
|
|
35
|
-
|
|
36
|
-
includeStreams: v.optional(v.boolean()),
|
|
85
|
+
code: v.string(),
|
|
37
86
|
},
|
|
38
87
|
returns: v.object({
|
|
39
88
|
connectionId: v.string(),
|
|
40
|
-
synced: v.number(),
|
|
89
|
+
synced: v.object({ athletes: v.number(), activities: v.number() }),
|
|
41
90
|
errors: v.array(
|
|
42
|
-
v.object({
|
|
91
|
+
v.object({ type: v.string(), id: v.string(), error: v.string() }),
|
|
43
92
|
),
|
|
44
93
|
}),
|
|
45
|
-
handler: async (ctx, args)
|
|
94
|
+
handler: async (ctx, args): Promise<{
|
|
95
|
+
connectionId: Id<"connections">;
|
|
96
|
+
synced: { athletes: number; activities: number };
|
|
97
|
+
errors: Array<{ type: string; id: string; error: string }>;
|
|
98
|
+
}> => {
|
|
46
99
|
// 1. Exchange authorization code for tokens
|
|
47
100
|
const tokens = await exchangeCode({
|
|
48
101
|
clientId: args.clientId,
|
|
49
102
|
clientSecret: args.clientSecret,
|
|
50
103
|
code: args.code,
|
|
51
|
-
baseUrl: args.baseUrl,
|
|
52
104
|
});
|
|
53
105
|
|
|
54
106
|
// 2. Create/reactivate the Soma connection
|
|
@@ -61,7 +113,7 @@ export const connectStrava = action({
|
|
|
61
113
|
);
|
|
62
114
|
|
|
63
115
|
// 3. Store OAuth tokens in providerTokens table
|
|
64
|
-
|
|
116
|
+
await ctx.runMutation(
|
|
65
117
|
internal.strava.private.storeTokens,
|
|
66
118
|
{
|
|
67
119
|
connectionId,
|
|
@@ -71,54 +123,109 @@ export const connectStrava = action({
|
|
|
71
123
|
},
|
|
72
124
|
);
|
|
73
125
|
|
|
74
|
-
// 4. Sync
|
|
75
|
-
const
|
|
126
|
+
// 4. Sync all data types
|
|
127
|
+
const result = await ctx.runAction(api.strava.public.syncAllTypes, {
|
|
76
128
|
accessToken: tokens.access_token,
|
|
77
|
-
|
|
129
|
+
connectionId,
|
|
130
|
+
userId: args.userId,
|
|
78
131
|
});
|
|
79
132
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
api.public.ingestAthlete,
|
|
133
|
+
// 5. Update lastDataUpdate timestamp
|
|
134
|
+
await ctx.runMutation(
|
|
135
|
+
api.public.updateConnection,
|
|
84
136
|
{
|
|
85
137
|
connectionId,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} as never,
|
|
138
|
+
lastDataUpdate: new Date().toISOString(),
|
|
139
|
+
},
|
|
89
140
|
);
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const errors: Array<{ activityId: number; error: string }> = [];
|
|
142
|
+
return { connectionId, synced: result.synced, errors: result.errors };
|
|
143
|
+
},
|
|
144
|
+
});
|
|
95
145
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Complete a Strava OAuth flow using stored pending state.
|
|
148
|
+
*
|
|
149
|
+
* Used by `registerRoutes` — the callback handler calls this with the
|
|
150
|
+
* `code` and `state` from the redirect. The action looks up the pending
|
|
151
|
+
* state (userId) stored during `getStravaAuthUrl`, exchanges for tokens,
|
|
152
|
+
* creates the connection, syncs data, and cleans up the pending entry.
|
|
153
|
+
*/
|
|
154
|
+
export const completeStravaOAuth = action({
|
|
155
|
+
args: {
|
|
156
|
+
code: v.string(),
|
|
157
|
+
state: v.string(),
|
|
158
|
+
clientId: v.string(),
|
|
159
|
+
clientSecret: v.string(),
|
|
160
|
+
},
|
|
161
|
+
returns: v.object({
|
|
162
|
+
connectionId: v.string(),
|
|
163
|
+
userId: v.string(),
|
|
164
|
+
synced: v.object({ athletes: v.number(), activities: v.number() }),
|
|
165
|
+
errors: v.array(
|
|
166
|
+
v.object({ type: v.string(), id: v.string(), error: v.string() }),
|
|
167
|
+
),
|
|
168
|
+
}),
|
|
169
|
+
handler: async (ctx, args): Promise<{
|
|
170
|
+
connectionId: Id<"connections">;
|
|
171
|
+
userId: string;
|
|
172
|
+
synced: { athletes: number; activities: number };
|
|
173
|
+
errors: Array<{ type: string; id: string; error: string }>;
|
|
174
|
+
}> => {
|
|
175
|
+
// 1. Look up pending state
|
|
176
|
+
const pending: Doc<"pendingOAuth"> | null = await ctx.runQuery(
|
|
177
|
+
internal.strava.private.getPendingOAuth,
|
|
178
|
+
{ state: args.state },
|
|
179
|
+
);
|
|
180
|
+
if (!pending) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"No pending Strava OAuth state found for this state parameter. " +
|
|
183
|
+
"The authorization may have expired or was already used.",
|
|
184
|
+
);
|
|
118
185
|
}
|
|
119
186
|
|
|
120
|
-
//
|
|
121
|
-
const
|
|
187
|
+
// 2. Exchange authorization code for tokens
|
|
188
|
+
const tokens = await exchangeCode({
|
|
189
|
+
clientId: args.clientId,
|
|
190
|
+
clientSecret: args.clientSecret,
|
|
191
|
+
code: args.code,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 3. Clean up pending entry
|
|
195
|
+
await ctx.runMutation(
|
|
196
|
+
internal.strava.private.deletePendingOAuth,
|
|
197
|
+
{ state: args.state },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// 4. Create/reactivate the Soma connection
|
|
201
|
+
const connectionId: Id<"connections"> = await ctx.runMutation(
|
|
202
|
+
api.public.connect,
|
|
203
|
+
{
|
|
204
|
+
userId: pending.userId,
|
|
205
|
+
provider: "STRAVA",
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 5. Store OAuth tokens
|
|
210
|
+
await ctx.runMutation(
|
|
211
|
+
internal.strava.private.storeTokens,
|
|
212
|
+
{
|
|
213
|
+
connectionId,
|
|
214
|
+
accessToken: tokens.access_token,
|
|
215
|
+
refreshToken: tokens.refresh_token,
|
|
216
|
+
expiresAt: tokens.expires_at,
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// 6. Sync all data types
|
|
221
|
+
const result = await ctx.runAction(api.strava.public.syncAllTypes, {
|
|
222
|
+
accessToken: tokens.access_token,
|
|
223
|
+
connectionId,
|
|
224
|
+
userId: pending.userId,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 7. Update lastDataUpdate timestamp
|
|
228
|
+
await ctx.runMutation(
|
|
122
229
|
api.public.updateConnection,
|
|
123
230
|
{
|
|
124
231
|
connectionId,
|
|
@@ -126,7 +233,12 @@ export const connectStrava = action({
|
|
|
126
233
|
},
|
|
127
234
|
);
|
|
128
235
|
|
|
129
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
connectionId,
|
|
238
|
+
userId: pending.userId,
|
|
239
|
+
synced: result.synced,
|
|
240
|
+
errors: result.errors,
|
|
241
|
+
};
|
|
130
242
|
},
|
|
131
243
|
});
|
|
132
244
|
|
|
@@ -143,17 +255,18 @@ export const syncStrava = action({
|
|
|
143
255
|
userId: v.string(),
|
|
144
256
|
clientId: v.string(),
|
|
145
257
|
clientSecret: v.string(),
|
|
146
|
-
baseUrl: v.optional(v.string()),
|
|
147
|
-
includeStreams: v.optional(v.boolean()),
|
|
148
258
|
after: v.optional(v.number()),
|
|
149
259
|
},
|
|
150
260
|
returns: v.object({
|
|
151
|
-
synced: v.number(),
|
|
261
|
+
synced: v.object({ athletes: v.number(), activities: v.number() }),
|
|
152
262
|
errors: v.array(
|
|
153
|
-
v.object({
|
|
263
|
+
v.object({ type: v.string(), id: v.string(), error: v.string() }),
|
|
154
264
|
),
|
|
155
265
|
}),
|
|
156
|
-
handler: async (ctx, args)
|
|
266
|
+
handler: async (ctx, args): Promise<{
|
|
267
|
+
synced: { athletes: number; activities: number };
|
|
268
|
+
errors: Array<{ type: string; id: string; error: string }>;
|
|
269
|
+
}> => {
|
|
157
270
|
// 1. Look up connection
|
|
158
271
|
const connection: Doc<"connections"> | null = await ctx.runQuery(
|
|
159
272
|
internal.private.getConnectionByProvider,
|
|
@@ -199,10 +312,9 @@ export const syncStrava = action({
|
|
|
199
312
|
clientId: args.clientId,
|
|
200
313
|
clientSecret: args.clientSecret,
|
|
201
314
|
refreshToken: tokenDoc.refreshToken,
|
|
202
|
-
baseUrl: args.baseUrl,
|
|
203
315
|
});
|
|
204
316
|
accessToken = refreshed.access_token;
|
|
205
|
-
|
|
317
|
+
await ctx.runMutation(
|
|
206
318
|
internal.strava.private.storeTokens,
|
|
207
319
|
{
|
|
208
320
|
connectionId,
|
|
@@ -213,68 +325,119 @@ export const syncStrava = action({
|
|
|
213
325
|
);
|
|
214
326
|
}
|
|
215
327
|
|
|
216
|
-
// 4.
|
|
217
|
-
const
|
|
328
|
+
// 4. Sync all data types
|
|
329
|
+
const result = await ctx.runAction(api.strava.public.syncAllTypes, {
|
|
218
330
|
accessToken,
|
|
219
|
-
|
|
331
|
+
connectionId,
|
|
332
|
+
userId: args.userId,
|
|
333
|
+
after: args.after,
|
|
220
334
|
});
|
|
221
335
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const _athleteId: Id<"athletes"> = await ctx.runMutation(
|
|
226
|
-
api.public.ingestAthlete,
|
|
336
|
+
// 5. Update lastDataUpdate timestamp
|
|
337
|
+
await ctx.runMutation(
|
|
338
|
+
api.public.updateConnection,
|
|
227
339
|
{
|
|
228
340
|
connectionId,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
} as never,
|
|
341
|
+
lastDataUpdate: new Date().toISOString(),
|
|
342
|
+
},
|
|
232
343
|
);
|
|
233
344
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
238
|
-
let synced = 0;
|
|
239
|
-
const errors: Array<{ activityId: number; error: string }> = [];
|
|
345
|
+
return { synced: result.synced, errors: result.errors };
|
|
346
|
+
},
|
|
347
|
+
});
|
|
240
348
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
349
|
+
// ─── Sync Engine ────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Fetch and ingest all Strava data types for a connected user.
|
|
353
|
+
*
|
|
354
|
+
* Called by the public actions (connectStrava, completeStravaOAuth, syncStrava)
|
|
355
|
+
* after obtaining a valid access token.
|
|
356
|
+
*/
|
|
357
|
+
export const syncAllTypes = action({
|
|
358
|
+
args: {
|
|
359
|
+
accessToken: v.string(),
|
|
360
|
+
connectionId: v.id("connections"),
|
|
361
|
+
userId: v.string(),
|
|
362
|
+
after: v.optional(v.number()),
|
|
363
|
+
before: v.optional(v.number()),
|
|
364
|
+
},
|
|
365
|
+
handler: async (ctx, args) => {
|
|
366
|
+
const { accessToken, connectionId, userId } = args;
|
|
367
|
+
const client = createStravaClient(accessToken);
|
|
368
|
+
|
|
369
|
+
const synced = { athletes: 0, activities: 0 };
|
|
370
|
+
const errors: Array<{ type: string; id: string; error: string }> = [];
|
|
371
|
+
|
|
372
|
+
// ── Athlete ──────────────────────────────────────────────────────────
|
|
373
|
+
try {
|
|
374
|
+
const { data: athlete, error } = await getLoggedInAthlete({ client });
|
|
375
|
+
if (error || !athlete) throw new Error(error ? JSON.stringify(error) : "No athlete data");
|
|
376
|
+
const data = transformAthlete(athlete);
|
|
377
|
+
await ctx.runMutation(api.public.ingestAthlete, {
|
|
378
|
+
connectionId,
|
|
379
|
+
userId,
|
|
380
|
+
...data,
|
|
381
|
+
} as never);
|
|
382
|
+
synced.athletes++;
|
|
383
|
+
} catch (err) {
|
|
384
|
+
errors.push({
|
|
385
|
+
type: "athlete",
|
|
386
|
+
id: "fetch",
|
|
387
|
+
error: err instanceof Error ? err.message : String(err),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Activities ───────────────────────────────────────────────────────
|
|
392
|
+
try {
|
|
393
|
+
const summaries = await listAllActivities(client, {
|
|
394
|
+
after: args.after,
|
|
395
|
+
before: args.before,
|
|
396
|
+
});
|
|
397
|
+
for (const summary of summaries) {
|
|
398
|
+
if (summary.id == null) continue;
|
|
399
|
+
try {
|
|
400
|
+
const { data: detailed, error: detailError } = await getActivityById({ client, path: { id: summary.id } });
|
|
401
|
+
if (detailError || !detailed) throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
|
|
402
|
+
|
|
403
|
+
const { data: streams } = await getActivityStreams({
|
|
404
|
+
client,
|
|
405
|
+
path: { id: summary.id },
|
|
406
|
+
query: {
|
|
407
|
+
keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
|
|
408
|
+
key_by_type: true,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const data = transformActivity(detailed, { streams: streams ?? undefined });
|
|
413
|
+
await ctx.runMutation(api.public.ingestActivity, {
|
|
251
414
|
connectionId,
|
|
252
|
-
userId
|
|
415
|
+
userId,
|
|
253
416
|
...data,
|
|
254
|
-
} as never
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
417
|
+
} as never);
|
|
418
|
+
synced.activities++;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
errors.push({
|
|
421
|
+
type: "activity",
|
|
422
|
+
id: String(summary.id),
|
|
423
|
+
error: err instanceof Error ? err.message : String(err),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
262
426
|
}
|
|
427
|
+
} catch (err) {
|
|
428
|
+
errors.push({
|
|
429
|
+
type: "activity",
|
|
430
|
+
id: "fetch",
|
|
431
|
+
error: err instanceof Error ? err.message : String(err),
|
|
432
|
+
});
|
|
263
433
|
}
|
|
264
434
|
|
|
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
435
|
return { synced, errors };
|
|
275
436
|
},
|
|
276
437
|
});
|
|
277
438
|
|
|
439
|
+
// ─── Disconnect ─────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
278
441
|
/**
|
|
279
442
|
* Disconnect a user from Strava.
|
|
280
443
|
*
|
|
@@ -286,7 +449,6 @@ export const disconnectStrava = action({
|
|
|
286
449
|
userId: v.string(),
|
|
287
450
|
clientId: v.string(),
|
|
288
451
|
clientSecret: v.string(),
|
|
289
|
-
baseUrl: v.optional(v.string()),
|
|
290
452
|
},
|
|
291
453
|
returns: v.null(),
|
|
292
454
|
handler: async (ctx, args) => {
|
|
@@ -310,11 +472,7 @@ export const disconnectStrava = action({
|
|
|
310
472
|
);
|
|
311
473
|
if (tokenDoc) {
|
|
312
474
|
try {
|
|
313
|
-
|
|
314
|
-
/\/+$/,
|
|
315
|
-
"",
|
|
316
|
-
);
|
|
317
|
-
await fetch(`${base}/oauth/deauthorize`, {
|
|
475
|
+
await fetch("https://www.strava.com/oauth/deauthorize", {
|
|
318
476
|
method: "POST",
|
|
319
477
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
320
478
|
body: `access_token=${tokenDoc.accessToken}`,
|