@nativesquare/soma 0.13.0 → 0.14.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/garmin.d.ts +4 -1
- package/dist/client/garmin.d.ts.map +1 -1
- package/dist/client/garmin.js +4 -1
- package/dist/client/garmin.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/strava.d.ts +48 -34
- package/dist/client/strava.d.ts.map +1 -1
- package/dist/client/strava.js +141 -23
- package/dist/client/strava.js.map +1 -1
- package/dist/client/types.d.ts +108 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +19 -17
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/auth.d.ts +2 -1
- package/dist/component/garmin/auth.d.ts.map +1 -1
- package/dist/component/garmin/auth.js +6 -1
- package/dist/component/garmin/auth.js.map +1 -1
- package/dist/component/garmin/private.d.ts +17 -75
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +4 -167
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +18 -33
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +23 -22
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +3 -6
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +17 -28
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/private.d.ts +59 -0
- package/dist/component/private.d.ts.map +1 -1
- package/dist/component/private.js +182 -1
- package/dist/component/private.js.map +1 -1
- package/dist/component/strava/auth.d.ts +2 -1
- package/dist/component/strava/auth.d.ts.map +1 -1
- package/dist/component/strava/auth.js +6 -1
- package/dist/component/strava/auth.js.map +1 -1
- package/dist/component/strava/public.d.ts +26 -50
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +88 -132
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/strava/webhooks.d.ts +17 -0
- package/dist/component/strava/webhooks.d.ts.map +1 -0
- package/dist/component/strava/webhooks.js +231 -0
- package/dist/component/strava/webhooks.js.map +1 -0
- package/dist/component/utils.d.ts +10 -0
- package/dist/component/utils.d.ts.map +1 -1
- package/dist/component/utils.js.map +1 -1
- package/dist/component/validators/athlete.d.ts +6 -0
- package/dist/component/validators/athlete.d.ts.map +1 -1
- package/dist/component/validators/athlete.js.map +1 -1
- package/dist/component/validators/nutrition.d.ts +6 -0
- package/dist/component/validators/nutrition.d.ts.map +1 -1
- package/dist/component/validators/nutrition.js.map +1 -1
- package/dist/component/validators/shared.d.ts +3 -0
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +1 -1
- package/dist/component/validators/shared.js.map +1 -1
- package/dist/component/validators/sleep.d.ts +6 -0
- package/dist/component/validators/sleep.d.ts.map +1 -1
- package/dist/component/validators/sleep.js.map +1 -1
- package/dist/validators.d.ts +7 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +6 -6
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +4 -1
- package/src/client/index.ts +8 -1
- package/src/client/strava.ts +193 -27
- package/src/client/types.ts +125 -0
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +25 -6
- package/src/component/garmin/auth.ts +9 -2
- package/src/component/garmin/private.ts +22 -243
- package/src/component/garmin/public.ts +56 -54
- package/src/component/garmin/webhooks.ts +38 -55
- package/src/component/private.ts +245 -1
- package/src/component/strava/auth.ts +9 -2
- package/src/component/strava/public.ts +105 -171
- package/src/component/strava/webhooks.ts +312 -0
- package/src/component/utils.ts +11 -0
- package/src/component/validators/athlete.ts +6 -0
- package/src/component/validators/nutrition.ts +6 -0
- package/src/component/validators/shared.ts +5 -2
- package/src/component/validators/sleep.ts +6 -0
- package/src/validators.ts +34 -7
- package/dist/component/strava/private.d.ts +0 -49
- package/dist/component/strava/private.d.ts.map +0 -1
- package/dist/component/strava/private.js +0 -121
- package/dist/component/strava/private.js.map +0 -1
- package/src/component/strava/private.ts +0 -147
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// ─── Strava Webhook Handler ─────────────────────────────────────────────────
|
|
2
|
+
// Single action that handles all Strava webhook event types.
|
|
3
|
+
// Strava sends notification-only payloads to a single endpoint — the handler
|
|
4
|
+
// fetches the actual data from the Strava API, transforms it, and optionally
|
|
5
|
+
// ingests it into the database.
|
|
6
|
+
|
|
7
|
+
import { v } from "convex/values";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { action, type ActionCtx } from "../_generated/server";
|
|
10
|
+
import { api, internal } from "../_generated/api";
|
|
11
|
+
import type { Doc } from "../_generated/dataModel";
|
|
12
|
+
import { createStravaClient } from "./client.js";
|
|
13
|
+
import {
|
|
14
|
+
getLoggedInAthlete,
|
|
15
|
+
getActivityById,
|
|
16
|
+
getActivityStreams,
|
|
17
|
+
} from "./types/stravaApi/sdk.gen.js";
|
|
18
|
+
import { transformActivity } from "./transform/activity.js";
|
|
19
|
+
import { transformAthlete } from "./transform/athlete.js";
|
|
20
|
+
import type { SomaError } from "../validators/shared.js";
|
|
21
|
+
|
|
22
|
+
// ─── Schema ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const stravaWebhookPayloadSchema = z.object({
|
|
25
|
+
object_type: z.string(),
|
|
26
|
+
object_id: z.number(),
|
|
27
|
+
aspect_type: z.string(),
|
|
28
|
+
owner_id: z.number(),
|
|
29
|
+
subscription_id: z.number(),
|
|
30
|
+
event_time: z.number(),
|
|
31
|
+
updates: z.record(z.string(), z.unknown()).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const VALID_EVENT_NAMES = new Set([
|
|
37
|
+
"activity-create",
|
|
38
|
+
"activity-update",
|
|
39
|
+
"activity-delete",
|
|
40
|
+
"athlete-update",
|
|
41
|
+
"athlete-deauthorize",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
type WebhookResult = {
|
|
45
|
+
errors: SomaError[];
|
|
46
|
+
items: Array<{ connectionId: string; userId: string; data: Record<string, unknown> | null }>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ─── Handler ────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export const handleStravaWebhook = action({
|
|
52
|
+
args: {
|
|
53
|
+
payload: v.any(),
|
|
54
|
+
clientId: v.string(),
|
|
55
|
+
clientSecret: v.string(),
|
|
56
|
+
autoIngest: v.optional(v.boolean()),
|
|
57
|
+
},
|
|
58
|
+
handler: async (ctx, args): Promise<WebhookResult> => {
|
|
59
|
+
const payload = stravaWebhookPayloadSchema.parse(args.payload);
|
|
60
|
+
const { clientId, clientSecret } = args;
|
|
61
|
+
const shouldIngest = args.autoIngest !== false;
|
|
62
|
+
const eventName = `${payload.object_type}-${payload.aspect_type}`;
|
|
63
|
+
|
|
64
|
+
if (!VALID_EVENT_NAMES.has(eventName)) {
|
|
65
|
+
return { errors: [], items: [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Resolve connection from owner_id ──────────────────────────────────
|
|
69
|
+
const connection: Doc<"connections"> | null = await ctx.runQuery(
|
|
70
|
+
internal.private.getConnectionByProviderUserId,
|
|
71
|
+
{ providerUserId: String(payload.owner_id), provider: "STRAVA" },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!connection) {
|
|
75
|
+
return {
|
|
76
|
+
errors: [{
|
|
77
|
+
type: payload.object_type,
|
|
78
|
+
id: String(payload.object_id),
|
|
79
|
+
message: `No Soma connection found for Strava owner_id "${payload.owner_id}". ` +
|
|
80
|
+
"The user may need to reconnect to populate the provider user ID.",
|
|
81
|
+
}],
|
|
82
|
+
items: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Allow deauthorize to proceed even if connection is inactive
|
|
87
|
+
if (!connection.active && eventName !== "athlete-deauthorize") {
|
|
88
|
+
return {
|
|
89
|
+
errors: [{
|
|
90
|
+
type: payload.object_type,
|
|
91
|
+
id: String(payload.object_id),
|
|
92
|
+
message: `Strava connection for owner_id "${payload.owner_id}" is inactive`,
|
|
93
|
+
}],
|
|
94
|
+
items: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const connectionId = connection._id;
|
|
99
|
+
const userId = connection.userId;
|
|
100
|
+
|
|
101
|
+
// ── Dispatch by event type ───────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
if (eventName === "activity-create" || eventName === "activity-update") {
|
|
104
|
+
return await handleActivityCreateOrUpdate(
|
|
105
|
+
ctx, { connectionId, userId, objectId: payload.object_id, clientId, clientSecret, shouldIngest },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (eventName === "activity-delete") {
|
|
110
|
+
return {
|
|
111
|
+
errors: [],
|
|
112
|
+
items: [{ connectionId, userId, data: null }],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (eventName === "athlete-update") {
|
|
117
|
+
return await handleAthleteUpdate(
|
|
118
|
+
ctx, { connectionId, userId, clientId, clientSecret, shouldIngest },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (eventName === "athlete-deauthorize") {
|
|
123
|
+
return await handleAthleteDeauthorize(
|
|
124
|
+
ctx, { connectionId, userId, shouldIngest },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { errors: [], items: [] };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── Event Handlers ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function handleActivityCreateOrUpdate(
|
|
135
|
+
ctx: ActionCtx,
|
|
136
|
+
args: {
|
|
137
|
+
connectionId: string;
|
|
138
|
+
userId: string;
|
|
139
|
+
objectId: number;
|
|
140
|
+
clientId: string;
|
|
141
|
+
clientSecret: string;
|
|
142
|
+
shouldIngest: boolean;
|
|
143
|
+
},
|
|
144
|
+
): Promise<WebhookResult> {
|
|
145
|
+
const errors: SomaError[] = [];
|
|
146
|
+
|
|
147
|
+
let accessToken: string;
|
|
148
|
+
try {
|
|
149
|
+
const resolved = await ctx.runAction(
|
|
150
|
+
internal.private.resolveConnectionAndAccessToken,
|
|
151
|
+
{ userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret },
|
|
152
|
+
);
|
|
153
|
+
accessToken = resolved.accessToken;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
errors: [{
|
|
157
|
+
type: "activity",
|
|
158
|
+
id: String(args.objectId),
|
|
159
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
160
|
+
}],
|
|
161
|
+
items: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const client = createStravaClient(accessToken);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const { data: detailed, error: detailError } = await getActivityById({
|
|
169
|
+
client,
|
|
170
|
+
path: { id: args.objectId },
|
|
171
|
+
});
|
|
172
|
+
if (detailError || !detailed) {
|
|
173
|
+
throw new Error(detailError ? JSON.stringify(detailError) : "No activity data");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { data: streams } = await getActivityStreams({
|
|
177
|
+
client,
|
|
178
|
+
path: { id: args.objectId },
|
|
179
|
+
query: {
|
|
180
|
+
keys: ["time", "heartrate", "watts", "cadence", "latlng", "altitude", "velocity_smooth", "grade_smooth", "distance", "temp"],
|
|
181
|
+
key_by_type: true,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const data = transformActivity(detailed, { streams: streams ?? undefined });
|
|
186
|
+
|
|
187
|
+
if (args.shouldIngest) {
|
|
188
|
+
await ctx.runMutation(api.public.ingestActivity, {
|
|
189
|
+
connectionId: args.connectionId,
|
|
190
|
+
userId: args.userId,
|
|
191
|
+
...data,
|
|
192
|
+
} as never);
|
|
193
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
194
|
+
connectionId: args.connectionId,
|
|
195
|
+
lastDataUpdate: new Date().toISOString(),
|
|
196
|
+
} as never);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
errors,
|
|
201
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
202
|
+
};
|
|
203
|
+
} catch (err) {
|
|
204
|
+
errors.push({
|
|
205
|
+
type: "activity",
|
|
206
|
+
id: String(args.objectId),
|
|
207
|
+
message: err instanceof Error ? err.message : String(err),
|
|
208
|
+
});
|
|
209
|
+
return { errors, items: [] };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function handleAthleteUpdate(
|
|
214
|
+
ctx: ActionCtx,
|
|
215
|
+
args: {
|
|
216
|
+
connectionId: string;
|
|
217
|
+
userId: string;
|
|
218
|
+
clientId: string;
|
|
219
|
+
clientSecret: string;
|
|
220
|
+
shouldIngest: boolean;
|
|
221
|
+
},
|
|
222
|
+
): Promise<WebhookResult> {
|
|
223
|
+
let accessToken: string;
|
|
224
|
+
try {
|
|
225
|
+
const resolved = await ctx.runAction(
|
|
226
|
+
internal.private.resolveConnectionAndAccessToken,
|
|
227
|
+
{ userId: args.userId, provider: "STRAVA", clientId: args.clientId, clientSecret: args.clientSecret },
|
|
228
|
+
);
|
|
229
|
+
accessToken = resolved.accessToken;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
errors: [{
|
|
233
|
+
type: "athlete",
|
|
234
|
+
id: "fetch",
|
|
235
|
+
message: `Token resolution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
236
|
+
}],
|
|
237
|
+
items: [],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const client = createStravaClient(accessToken);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const { data: athlete, error } = await getLoggedInAthlete({ client });
|
|
245
|
+
if (error || !athlete) {
|
|
246
|
+
throw new Error(error ? JSON.stringify(error) : "No athlete data");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const data = transformAthlete(athlete);
|
|
250
|
+
|
|
251
|
+
if (args.shouldIngest) {
|
|
252
|
+
await ctx.runMutation(api.public.ingestAthlete, {
|
|
253
|
+
connectionId: args.connectionId,
|
|
254
|
+
userId: args.userId,
|
|
255
|
+
...data,
|
|
256
|
+
} as never);
|
|
257
|
+
await ctx.runMutation(api.public.updateConnection, {
|
|
258
|
+
connectionId: args.connectionId,
|
|
259
|
+
lastDataUpdate: new Date().toISOString(),
|
|
260
|
+
} as never);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
errors: [],
|
|
265
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data }],
|
|
266
|
+
};
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
errors: [{
|
|
270
|
+
type: "athlete",
|
|
271
|
+
id: "fetch",
|
|
272
|
+
message: err instanceof Error ? err.message : String(err),
|
|
273
|
+
}],
|
|
274
|
+
items: [],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function handleAthleteDeauthorize(
|
|
280
|
+
ctx: ActionCtx,
|
|
281
|
+
args: {
|
|
282
|
+
connectionId: string;
|
|
283
|
+
userId: string;
|
|
284
|
+
shouldIngest: boolean;
|
|
285
|
+
},
|
|
286
|
+
): Promise<WebhookResult> {
|
|
287
|
+
if (args.shouldIngest) {
|
|
288
|
+
try {
|
|
289
|
+
await ctx.runMutation(internal.private.deleteTokens, {
|
|
290
|
+
connectionId: args.connectionId,
|
|
291
|
+
} as never);
|
|
292
|
+
await ctx.runMutation(api.public.disconnect, {
|
|
293
|
+
userId: args.userId,
|
|
294
|
+
provider: "STRAVA",
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return {
|
|
298
|
+
errors: [{
|
|
299
|
+
type: "athlete",
|
|
300
|
+
id: "deauthorize",
|
|
301
|
+
message: err instanceof Error ? err.message : String(err),
|
|
302
|
+
}],
|
|
303
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
errors: [],
|
|
310
|
+
items: [{ connectionId: args.connectionId, userId: args.userId, data: null }],
|
|
311
|
+
};
|
|
312
|
+
}
|
package/src/component/utils.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// ─── Shared Helpers ─────────────────────────────────────────────────────────
|
|
2
2
|
// Provider-agnostic utilities shared across providers (Strava, Garmin, etc.).
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Normalized result from any provider's OAuth refresh-token call.
|
|
6
|
+
* Each provider's `refreshToken` maps its raw API response into this shape.
|
|
7
|
+
*/
|
|
8
|
+
export interface OAuthRefreshResult {
|
|
9
|
+
access_token: string;
|
|
10
|
+
refresh_token: string;
|
|
11
|
+
/** Absolute Unix timestamp (seconds) when the access token expires. */
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* Generate a random state parameter for CSRF protection.
|
|
6
17
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Infer } from "convex/values";
|
|
1
2
|
import { v } from "convex/values";
|
|
2
3
|
|
|
3
4
|
// ─── Athlete ─────────────────────────────────────────────────────────────────
|
|
@@ -23,3 +24,8 @@ export const athleteValidator = {
|
|
|
23
24
|
joined_provider: v.optional(v.string()),
|
|
24
25
|
devices: v.optional(v.array(v.any())),
|
|
25
26
|
};
|
|
27
|
+
|
|
28
|
+
/** Data-only shape (no connectionId / userId). */
|
|
29
|
+
type AthleteData = Omit<typeof athleteValidator, "connectionId" | "userId">;
|
|
30
|
+
|
|
31
|
+
export type SomaAthlete = Infer<ReturnType<typeof v.object<AthleteData>>>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Infer } from "convex/values";
|
|
1
2
|
import { v } from "convex/values";
|
|
2
3
|
import { drinkSample, meal } from "./samples.js";
|
|
3
4
|
import { macros, micros } from "./shared.js";
|
|
@@ -35,3 +36,8 @@ export const nutritionValidator = {
|
|
|
35
36
|
}),
|
|
36
37
|
),
|
|
37
38
|
};
|
|
39
|
+
|
|
40
|
+
/** Data-only shape (no connectionId / userId). */
|
|
41
|
+
type NutritionData = Omit<typeof nutritionValidator, "connectionId" | "userId">;
|
|
42
|
+
|
|
43
|
+
export type SomaNutrition = Infer<ReturnType<typeof v.object<NutritionData>>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { v } from "convex/values";
|
|
1
|
+
import { type Infer, v } from "convex/values";
|
|
2
2
|
|
|
3
|
-
// ─── SomaError
|
|
3
|
+
// ─── SomaError ──────────────────────────────────────────────────────────────
|
|
4
4
|
// Convex validator matching the SomaError TypeScript interface.
|
|
5
5
|
export const somaErrorValidator = v.object({
|
|
6
6
|
type: v.string(),
|
|
@@ -8,6 +8,9 @@ export const somaErrorValidator = v.object({
|
|
|
8
8
|
message: v.string(),
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
/** Structured error from a Soma operation, derived from the Convex validator. */
|
|
12
|
+
export type SomaError = Infer<typeof somaErrorValidator>;
|
|
13
|
+
|
|
11
14
|
import {
|
|
12
15
|
heartRateDataSample,
|
|
13
16
|
hrvSampleRmssd,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Infer } from "convex/values";
|
|
1
2
|
import { v } from "convex/values";
|
|
2
3
|
import {
|
|
3
4
|
sleepHypnogramSample,
|
|
@@ -131,3 +132,8 @@ export const sleepValidator = {
|
|
|
131
132
|
}),
|
|
132
133
|
),
|
|
133
134
|
};
|
|
135
|
+
|
|
136
|
+
/** Data-only shape (no connectionId / userId). */
|
|
137
|
+
type SleepData = Omit<typeof sleepValidator, "connectionId" | "userId">;
|
|
138
|
+
|
|
139
|
+
export type SomaSleep = Infer<ReturnType<typeof v.object<SleepData>>>;
|
package/src/validators.ts
CHANGED
|
@@ -41,20 +41,38 @@
|
|
|
41
41
|
import { v } from "convex/values";
|
|
42
42
|
export { somaErrorValidator } from "./component/validators/shared.js";
|
|
43
43
|
import { connectionValidator as _connectionValidator } from "./component/validators/connection.js";
|
|
44
|
-
import { athleteValidator as _athleteValidator } from "./component/validators/athlete.js";
|
|
45
44
|
import {
|
|
46
45
|
activityValidator as _activityValidator,
|
|
47
46
|
type SomaActivity,
|
|
48
47
|
} from "./component/validators/activity.js";
|
|
48
|
+
import {
|
|
49
|
+
athleteValidator as _athleteValidator,
|
|
50
|
+
type SomaAthlete,
|
|
51
|
+
} from "./component/validators/athlete.js";
|
|
49
52
|
import {
|
|
50
53
|
bodyValidator as _bodyValidator,
|
|
51
54
|
type SomaBody,
|
|
52
55
|
} from "./component/validators/body.js";
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
dailyValidator as _dailyValidator,
|
|
58
|
+
type SomaDaily,
|
|
59
|
+
} from "./component/validators/daily.js";
|
|
60
|
+
import {
|
|
61
|
+
sleepValidator as _sleepValidator,
|
|
62
|
+
type SomaSleep,
|
|
63
|
+
} from "./component/validators/sleep.js";
|
|
64
|
+
import {
|
|
65
|
+
menstruationValidator as _menstruationValidator,
|
|
66
|
+
type SomaMenstruation,
|
|
67
|
+
} from "./component/validators/menstruation.js";
|
|
68
|
+
import {
|
|
69
|
+
nutritionValidator as _nutritionValidator,
|
|
70
|
+
type SomaNutrition,
|
|
71
|
+
} from "./component/validators/nutrition.js";
|
|
72
|
+
import {
|
|
73
|
+
plannedWorkoutValidator as _plannedWorkoutValidator,
|
|
74
|
+
type SomaPlannedWorkout,
|
|
75
|
+
} from "./component/validators/plannedWorkout.js";
|
|
58
76
|
|
|
59
77
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
60
78
|
|
|
@@ -97,4 +115,13 @@ export const plannedWorkoutData = stripConnection(_plannedWorkoutValidator);
|
|
|
97
115
|
|
|
98
116
|
// ─── Soma types ─────────────────────────────────────────────────────────────
|
|
99
117
|
|
|
100
|
-
export type {
|
|
118
|
+
export type {
|
|
119
|
+
SomaActivity,
|
|
120
|
+
SomaAthlete,
|
|
121
|
+
SomaBody,
|
|
122
|
+
SomaDaily,
|
|
123
|
+
SomaMenstruation,
|
|
124
|
+
SomaNutrition,
|
|
125
|
+
SomaPlannedWorkout,
|
|
126
|
+
SomaSleep,
|
|
127
|
+
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export declare const storePendingOAuth: import("convex/server").RegisteredMutation<"internal", {
|
|
2
|
-
state: string;
|
|
3
|
-
userId: string;
|
|
4
|
-
provider: string;
|
|
5
|
-
}, Promise<null>>;
|
|
6
|
-
export declare const getPendingOAuth: import("convex/server").RegisteredQuery<"internal", {
|
|
7
|
-
state: string;
|
|
8
|
-
}, Promise<{
|
|
9
|
-
_id: import("convex/values").GenericId<"pendingOAuth">;
|
|
10
|
-
_creationTime: number;
|
|
11
|
-
codeVerifier?: string | undefined;
|
|
12
|
-
state: string;
|
|
13
|
-
userId: string;
|
|
14
|
-
provider: string;
|
|
15
|
-
createdAt: number;
|
|
16
|
-
} | null>>;
|
|
17
|
-
export declare const deletePendingOAuth: import("convex/server").RegisteredMutation<"internal", {
|
|
18
|
-
state: string;
|
|
19
|
-
}, Promise<null>>;
|
|
20
|
-
/**
|
|
21
|
-
* Store or update OAuth tokens for a connection.
|
|
22
|
-
* Upserts by connectionId — one token record per connection.
|
|
23
|
-
*/
|
|
24
|
-
export declare const storeTokens: import("convex/server").RegisteredMutation<"internal", {
|
|
25
|
-
connectionId: import("convex/values").GenericId<"connections">;
|
|
26
|
-
accessToken: string;
|
|
27
|
-
refreshToken: string;
|
|
28
|
-
expiresAt: number;
|
|
29
|
-
}, Promise<null>>;
|
|
30
|
-
/**
|
|
31
|
-
* Get stored tokens for a connection.
|
|
32
|
-
*/
|
|
33
|
-
export declare const getTokens: import("convex/server").RegisteredQuery<"internal", {
|
|
34
|
-
connectionId: import("convex/values").GenericId<"connections">;
|
|
35
|
-
}, Promise<{
|
|
36
|
-
_id: import("convex/values").GenericId<"providerTokens">;
|
|
37
|
-
_creationTime: number;
|
|
38
|
-
refreshToken?: string | undefined;
|
|
39
|
-
expiresAt?: number | undefined;
|
|
40
|
-
connectionId: import("convex/values").GenericId<"connections">;
|
|
41
|
-
accessToken: string;
|
|
42
|
-
} | null>>;
|
|
43
|
-
/**
|
|
44
|
-
* Delete stored tokens for a connection.
|
|
45
|
-
*/
|
|
46
|
-
export declare const deleteTokens: import("convex/server").RegisteredMutation<"internal", {
|
|
47
|
-
connectionId: import("convex/values").GenericId<"connections">;
|
|
48
|
-
}, Promise<null>>;
|
|
49
|
-
//# sourceMappingURL=private.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../../src/component/strava/private.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,iBAAiB;;;;iBAc5B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;UAmB1B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;iBAa7B,CAAC;AAIH;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;iBA4BtB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;UAqBpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,YAAY;;iBAgBvB,CAAC"}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// ─── Internal Token & Pending OAuth CRUD ─────────────────────────────────────
|
|
2
|
-
// Called only from strava/public.ts. Not exposed to the host app.
|
|
3
|
-
import { v } from "convex/values";
|
|
4
|
-
import { internalMutation, internalQuery, } from "../_generated/server";
|
|
5
|
-
// ─── Internal Pending OAuth CRUD ─────────────────────────────────────────────
|
|
6
|
-
// Temporary storage for in-progress Strava OAuth flows.
|
|
7
|
-
// Bridges getStravaAuthUrl and completeStravaOAuth.
|
|
8
|
-
export const storePendingOAuth = internalMutation({
|
|
9
|
-
args: {
|
|
10
|
-
provider: v.string(),
|
|
11
|
-
state: v.string(),
|
|
12
|
-
userId: v.string(),
|
|
13
|
-
},
|
|
14
|
-
returns: v.null(),
|
|
15
|
-
handler: async (ctx, args) => {
|
|
16
|
-
await ctx.db.insert("pendingOAuth", {
|
|
17
|
-
...args,
|
|
18
|
-
createdAt: Date.now(),
|
|
19
|
-
});
|
|
20
|
-
return null;
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
export const getPendingOAuth = internalQuery({
|
|
24
|
-
args: { state: v.string() },
|
|
25
|
-
returns: v.union(v.object({
|
|
26
|
-
_id: v.id("pendingOAuth"),
|
|
27
|
-
_creationTime: v.number(),
|
|
28
|
-
provider: v.string(),
|
|
29
|
-
state: v.string(),
|
|
30
|
-
userId: v.string(),
|
|
31
|
-
createdAt: v.number(),
|
|
32
|
-
}), v.null()),
|
|
33
|
-
handler: async (ctx, args) => {
|
|
34
|
-
return await ctx.db
|
|
35
|
-
.query("pendingOAuth")
|
|
36
|
-
.withIndex("by_state", (q) => q.eq("state", args.state))
|
|
37
|
-
.first();
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
export const deletePendingOAuth = internalMutation({
|
|
41
|
-
args: { state: v.string() },
|
|
42
|
-
returns: v.null(),
|
|
43
|
-
handler: async (ctx, args) => {
|
|
44
|
-
const pending = await ctx.db
|
|
45
|
-
.query("pendingOAuth")
|
|
46
|
-
.withIndex("by_state", (q) => q.eq("state", args.state))
|
|
47
|
-
.first();
|
|
48
|
-
if (pending) {
|
|
49
|
-
await ctx.db.delete(pending._id);
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
// ─── Internal Token CRUD ─────────────────────────────────────────────────────
|
|
55
|
-
/**
|
|
56
|
-
* Store or update OAuth tokens for a connection.
|
|
57
|
-
* Upserts by connectionId — one token record per connection.
|
|
58
|
-
*/
|
|
59
|
-
export const storeTokens = internalMutation({
|
|
60
|
-
args: {
|
|
61
|
-
connectionId: v.id("connections"),
|
|
62
|
-
accessToken: v.string(),
|
|
63
|
-
refreshToken: v.string(),
|
|
64
|
-
expiresAt: v.number(),
|
|
65
|
-
},
|
|
66
|
-
returns: v.null(),
|
|
67
|
-
handler: async (ctx, args) => {
|
|
68
|
-
const existing = await ctx.db
|
|
69
|
-
.query("providerTokens")
|
|
70
|
-
.withIndex("by_connectionId", (q) => q.eq("connectionId", args.connectionId))
|
|
71
|
-
.first();
|
|
72
|
-
if (existing) {
|
|
73
|
-
await ctx.db.patch(existing._id, {
|
|
74
|
-
accessToken: args.accessToken,
|
|
75
|
-
refreshToken: args.refreshToken,
|
|
76
|
-
expiresAt: args.expiresAt,
|
|
77
|
-
});
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
await ctx.db.insert("providerTokens", args);
|
|
81
|
-
return null;
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
/**
|
|
85
|
-
* Get stored tokens for a connection.
|
|
86
|
-
*/
|
|
87
|
-
export const getTokens = internalQuery({
|
|
88
|
-
args: { connectionId: v.id("connections") },
|
|
89
|
-
returns: v.union(v.object({
|
|
90
|
-
_id: v.id("providerTokens"),
|
|
91
|
-
_creationTime: v.number(),
|
|
92
|
-
connectionId: v.id("connections"),
|
|
93
|
-
accessToken: v.string(),
|
|
94
|
-
refreshToken: v.optional(v.string()),
|
|
95
|
-
expiresAt: v.optional(v.number()),
|
|
96
|
-
}), v.null()),
|
|
97
|
-
handler: async (ctx, args) => {
|
|
98
|
-
return await ctx.db
|
|
99
|
-
.query("providerTokens")
|
|
100
|
-
.withIndex("by_connectionId", (q) => q.eq("connectionId", args.connectionId))
|
|
101
|
-
.first();
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
/**
|
|
105
|
-
* Delete stored tokens for a connection.
|
|
106
|
-
*/
|
|
107
|
-
export const deleteTokens = internalMutation({
|
|
108
|
-
args: { connectionId: v.id("connections") },
|
|
109
|
-
returns: v.null(),
|
|
110
|
-
handler: async (ctx, args) => {
|
|
111
|
-
const existing = await ctx.db
|
|
112
|
-
.query("providerTokens")
|
|
113
|
-
.withIndex("by_connectionId", (q) => q.eq("connectionId", args.connectionId))
|
|
114
|
-
.first();
|
|
115
|
-
if (existing) {
|
|
116
|
-
await ctx.db.delete(existing._id);
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
//# sourceMappingURL=private.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"private.js","sourceRoot":"","sources":["../../../src/component/strava/private.ts"],"names":[],"mappings":"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"}
|