@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.
- package/dist/client/index.d.ts +124 -144
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +157 -134
- 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 +113 -22
- 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 +10 -1
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +49 -9
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +237 -62
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +689 -254
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/utils.d.ts +8 -0
- package/dist/component/garmin/utils.d.ts.map +1 -1
- package/dist/component/garmin/utils.js +9 -0
- package/dist/component/garmin/utils.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 +54 -19
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +159 -109
- 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 +31 -45
- 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 +285 -164
- package/src/component/_generated/api.ts +18 -0
- package/src/component/_generated/component.ts +191 -24
- package/src/component/garmin/auth.ts +0 -9
- package/src/component/garmin/private.ts +84 -12
- package/src/component/garmin/public.ts +812 -348
- package/src/component/garmin/utils.ts +17 -0
- 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 +191 -139
- 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,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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
|
117
|
+
userId: pending.userId,
|
|
59
118
|
provider: "STRAVA",
|
|
60
119
|
},
|
|
61
120
|
);
|
|
62
121
|
|
|
63
|
-
//
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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({
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
217
|
-
const
|
|
223
|
+
// 4. Sync all data types
|
|
224
|
+
const result = await ctx.runAction(api.strava.public.syncAllTypes, {
|
|
218
225
|
accessToken,
|
|
219
|
-
|
|
226
|
+
connectionId,
|
|
227
|
+
userId: args.userId,
|
|
228
|
+
after: args.after,
|
|
220
229
|
});
|
|
221
230
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
} as never,
|
|
236
|
+
lastDataUpdate: new Date().toISOString(),
|
|
237
|
+
},
|
|
232
238
|
);
|
|
233
239
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
309
|
+
userId,
|
|
253
310
|
...data,
|
|
254
|
-
} as never
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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}`,
|