@nativesquare/soma 0.10.2 → 0.12.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.
@@ -0,0 +1,108 @@
1
+ import type { SomaComponent } from "./index.js";
2
+ import type { ActionCtx, SomaStravaConfig } from "./types.js";
3
+
4
+ export class SomaStrava {
5
+ constructor(
6
+ private component: SomaComponent,
7
+ private requireConfig: () => SomaStravaConfig,
8
+ ) {}
9
+
10
+ /**
11
+ * Generate a Strava OAuth authorization URL.
12
+ *
13
+ * The state parameter is stored inside the component automatically,
14
+ * and the callback handler registered by `registerRoutes` will
15
+ * complete the flow without further host-app intervention.
16
+ *
17
+ * @param ctx - Action context from the host app
18
+ * @param opts.userId - The host app's user identifier
19
+ * @param opts.redirectUri - The URL Strava will redirect to after authorization
20
+ * @param opts.scope - Comma-separated Strava OAuth scopes (default: "read,activity:read_all,profile:read_all")
21
+ * @returns `{ authUrl, state }`
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const { authUrl } = await soma.strava.getAuthUrl(ctx, {
26
+ * userId: "user_123",
27
+ * redirectUri: "https://your-app.convex.site/api/strava/callback",
28
+ * });
29
+ * // Redirect user to authUrl — the callback is handled automatically
30
+ * ```
31
+ */
32
+ async getAuthUrl(
33
+ ctx: ActionCtx,
34
+ opts: { userId: string; redirectUri: string; scope?: string },
35
+ ) {
36
+ const config = this.requireConfig();
37
+ return await ctx.runAction(this.component.strava.public.getStravaAuthUrl, {
38
+ clientId: config.clientId,
39
+ redirectUri: opts.redirectUri,
40
+ scope: opts.scope,
41
+ userId: opts.userId,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Sync activities from Strava for an already-connected user.
47
+ *
48
+ * Automatically refreshes the access token if expired. Fetches the
49
+ * athlete profile and activities, transforms them, and ingests into Soma.
50
+ *
51
+ * @param ctx - Action context from the host app
52
+ * @param args.userId - The host app's user identifier
53
+ * @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
54
+ * @returns `{ synced, errors }`
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * export const syncStrava = action({
59
+ * args: { userId: v.string() },
60
+ * handler: async (ctx, { userId }) => {
61
+ * return await soma.strava.sync(ctx, { userId });
62
+ * },
63
+ * });
64
+ * ```
65
+ */
66
+ async sync(
67
+ ctx: ActionCtx,
68
+ args: { userId: string; after?: number },
69
+ ) {
70
+ const config = this.requireConfig();
71
+ return await ctx.runAction(this.component.strava.public.syncStrava, {
72
+ ...args,
73
+ clientId: config.clientId,
74
+ clientSecret: config.clientSecret,
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Disconnect a user from Strava.
80
+ *
81
+ * Revokes the token at Strava (best-effort), deletes stored tokens,
82
+ * and sets the connection to inactive.
83
+ *
84
+ * @param ctx - Action context from the host app
85
+ * @param args.userId - The host app's user identifier
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * export const disconnectStrava = action({
90
+ * args: { userId: v.string() },
91
+ * handler: async (ctx, { userId }) => {
92
+ * await soma.strava.disconnect(ctx, { userId });
93
+ * },
94
+ * });
95
+ * ```
96
+ */
97
+ async disconnect(
98
+ ctx: ActionCtx,
99
+ args: { userId: string },
100
+ ) {
101
+ const config = this.requireConfig();
102
+ return await ctx.runAction(this.component.strava.public.disconnectStrava, {
103
+ ...args,
104
+ clientId: config.clientId,
105
+ clientSecret: config.clientSecret,
106
+ });
107
+ }
108
+ }
@@ -1,18 +1,215 @@
1
- import type {
2
- GenericActionCtx,
3
- GenericMutationCtx,
4
- GenericQueryCtx,
5
- GenericDataModel,
6
- } from "convex/server";
7
-
8
- export type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
9
-
10
- export type MutationCtx = Pick<
11
- GenericMutationCtx<GenericDataModel>,
12
- "runQuery" | "runMutation"
13
- >;
14
-
15
- export type ActionCtx = Pick<
16
- GenericActionCtx<GenericDataModel>,
17
- "runQuery" | "runMutation" | "runAction"
18
- >;
1
+ import type {
2
+ GenericActionCtx,
3
+ GenericMutationCtx,
4
+ GenericQueryCtx,
5
+ GenericDataModel,
6
+ } from "convex/server";
7
+
8
+ // ─── Context Types ──────────────────────────────────────────────────────────
9
+ // Narrowed Convex context types that only expose the runner methods each
10
+ // operation actually needs.
11
+
12
+ export type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
13
+
14
+ export type MutationCtx = Pick<
15
+ GenericMutationCtx<GenericDataModel>,
16
+ "runQuery" | "runMutation"
17
+ >;
18
+
19
+ export type ActionCtx = Pick<
20
+ GenericActionCtx<GenericDataModel>,
21
+ "runQuery" | "runMutation" | "runAction"
22
+ >;
23
+
24
+ // ─── Provider Configuration ─────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Configuration for the Strava integration.
28
+ *
29
+ * If not provided to the Soma constructor, the class will attempt to
30
+ * read `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET`
31
+ * from environment variables automatically.
32
+ */
33
+ export interface SomaStravaConfig {
34
+ /** Your Strava application's Client ID. */
35
+ clientId: string;
36
+ /** Your Strava application's Client Secret. */
37
+ clientSecret: string;
38
+ }
39
+
40
+ /**
41
+ * Configuration for the Garmin integration.
42
+ *
43
+ * If not provided to the Soma constructor, the class will attempt to
44
+ * read `GARMIN_CLIENT_ID` and `GARMIN_CLIENT_SECRET` from
45
+ * environment variables automatically.
46
+ */
47
+ export interface SomaGarminConfig {
48
+ /** Your Garmin application's Client ID. */
49
+ clientId: string;
50
+ /** Your Garmin application's Client Secret. */
51
+ clientSecret: string;
52
+ }
53
+
54
+ // ─── Data Query & Ingestion Args ────────────────────────────────────────────
55
+
56
+ /**
57
+ * Common args shape for all ingestion methods.
58
+ *
59
+ * Requires `connectionId` and `userId` at minimum — additional fields
60
+ * come from the transformer output (e.g., `metadata`, `calories_data`, etc.)
61
+ * and are validated server-side by Convex validators.
62
+ */
63
+ export type IngestArgs = {
64
+ connectionId: string;
65
+ userId: string;
66
+ } & Record<string, unknown>;
67
+
68
+ /**
69
+ * Base args for time-range filtered queries.
70
+ *
71
+ * - `userId` is required for all health data queries.
72
+ * - `startTime` / `endTime` are optional ISO-8601 bounds on `metadata.start_time`.
73
+ */
74
+ export type TimeRangeArgs = {
75
+ userId: string;
76
+ startTime?: string;
77
+ endTime?: string;
78
+ };
79
+
80
+ /**
81
+ * Args for list (collect-all) queries with optional ordering and limit.
82
+ */
83
+ export type ListTimeRangeArgs = TimeRangeArgs & {
84
+ order?: "asc" | "desc";
85
+ limit?: number;
86
+ };
87
+
88
+ /**
89
+ * Args for paginated queries with Convex pagination options.
90
+ */
91
+ export type PaginateTimeRangeArgs = TimeRangeArgs & {
92
+ paginationOpts: { numItems: number; cursor: string | null };
93
+ };
94
+
95
+ // ─── OAuth Callback Events ──────────────────────────────────────────────────
96
+
97
+ /** Data passed to `onComplete` after Strava OAuth completes. */
98
+ export interface StravaConnectEvent {
99
+ provider: "STRAVA";
100
+ userId: string;
101
+ connectionId: string;
102
+ }
103
+
104
+ /** Data passed to `oauth.onComplete` after Garmin OAuth completes. */
105
+ export interface GarminConnectEvent {
106
+ provider: "GARMIN";
107
+ userId: string;
108
+ connectionId: string;
109
+ }
110
+
111
+ // ─── Garmin Webhook Types ───────────────────────────────────────────────────
112
+
113
+ /** Data passed to webhook `events` handlers and `onEvent` after data ingestion. */
114
+ export interface GarminWebhookEvent {
115
+ dataType: string;
116
+ processed: number;
117
+ errors: Array<{ type: string; id: string; error: string }>;
118
+ /** Users whose data was affected by this webhook. */
119
+ affectedUsers: Array<{ userId: string; connectionId: string }>;
120
+ }
121
+
122
+ /** Webhook endpoint names matching the Garmin API data types. */
123
+ export type GarminWebhookEventName =
124
+ | "activities" | "activity-details" | "manually-updated-activities" | "move-iq"
125
+ | "blood-pressures" | "body-compositions" | "dailies" | "epochs"
126
+ | "health-snapshot" | "sleeps" | "hrv" | "stress" | "pulse-ox"
127
+ | "respiration" | "skin-temp" | "user-metrics" | "menstrual-cycle-tracking";
128
+
129
+ /** Handler for a specific webhook event or the catch-all `onEvent`. */
130
+ export type GarminWebhookHandler = (
131
+ ctx: GenericActionCtx<GenericDataModel>,
132
+ event: GarminWebhookEvent,
133
+ ) => Promise<void>;
134
+
135
+ // ─── Route Registration Options ─────────────────────────────────────────────
136
+
137
+ /**
138
+ * Per-provider options for `registerRoutes`.
139
+ */
140
+ export interface StravaOAuthOptions {
141
+ /** HTTP path for the OAuth callback. @default "/api/strava/callback" */
142
+ path?: string;
143
+ /** Override STRAVA_CLIENT_ID env var. */
144
+ clientId?: string;
145
+ /** Override STRAVA_CLIENT_SECRET env var. */
146
+ clientSecret?: string;
147
+ /** URL to redirect the user to after a successful connection. */
148
+ redirectTo?: string;
149
+ /** Called after Strava OAuth completes and the connection is established. */
150
+ onComplete?: (
151
+ ctx: GenericActionCtx<GenericDataModel>,
152
+ event: StravaConnectEvent,
153
+ ) => Promise<void>;
154
+ }
155
+
156
+ export interface GarminOAuthOptions {
157
+ /** HTTP path for the OAuth callback. @default "/api/garmin/callback" */
158
+ path?: string;
159
+ /** Override GARMIN_CLIENT_ID env var. */
160
+ clientId?: string;
161
+ /** Override GARMIN_CLIENT_SECRET env var. */
162
+ clientSecret?: string;
163
+ /** URL to redirect the user to after a successful connection. */
164
+ redirectTo?: string;
165
+ /** Called after Garmin OAuth completes and the connection is established. */
166
+ onComplete?: (
167
+ ctx: GenericActionCtx<GenericDataModel>,
168
+ event: GarminConnectEvent,
169
+ ) => Promise<void>;
170
+ }
171
+
172
+ export interface GarminWebhookOptions {
173
+ /** Base path prefix for all webhook routes. @default "/api/garmin/webhook" */
174
+ basePath?: string;
175
+ /** Called after every webhook payload is processed, regardless of data type. */
176
+ onEvent?: GarminWebhookHandler;
177
+ /**
178
+ * Per-data-type webhook registration.
179
+ *
180
+ * **Only data types listed here get an HTTP route registered.**
181
+ * Unlisted types are ignored — Garmin receives a 404 if it POSTs to them.
182
+ *
183
+ * Pass a handler function to run custom logic after ingestion,
184
+ * or `true` to register the route with default processing only.
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * events: {
189
+ * "activities": async (ctx, event) => { // custom side-effect },
190
+ * "sleeps": true, // register route, default processing only
191
+ * "dailies": true,
192
+ * }
193
+ * ```
194
+ */
195
+ events?: Partial<Record<GarminWebhookEventName, GarminWebhookHandler | true>>;
196
+ }
197
+
198
+ export interface RegisterRoutesOptions {
199
+ strava?: {
200
+ /** OAuth callback configuration. */
201
+ oauth?: StravaOAuthOptions;
202
+ };
203
+ garmin?: {
204
+ /** OAuth callback configuration. */
205
+ oauth?: GarminOAuthOptions;
206
+ /**
207
+ * Webhook route configuration.
208
+ *
209
+ * Routes are **disabled by default**. Only data types listed in `events`
210
+ * get an HTTP endpoint registered. Omit entirely or set to `false` to
211
+ * skip all webhook routes.
212
+ */
213
+ webhook?: GarminWebhookOptions | false;
214
+ };
215
+ }
@@ -38,6 +38,30 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
38
38
  any,
39
39
  Name
40
40
  >;
41
+ deleteSchedule: FunctionReference<
42
+ "action",
43
+ "internal",
44
+ {
45
+ clientId: string;
46
+ clientSecret: string;
47
+ plannedWorkoutId: string;
48
+ userId: string;
49
+ },
50
+ any,
51
+ Name
52
+ >;
53
+ deleteWorkout: FunctionReference<
54
+ "action",
55
+ "internal",
56
+ {
57
+ clientId: string;
58
+ clientSecret: string;
59
+ plannedWorkoutId: string;
60
+ userId: string;
61
+ },
62
+ any,
63
+ Name
64
+ >;
41
65
  disconnectGarmin: FunctionReference<
42
66
  "action",
43
67
  "internal",
@@ -221,41 +245,28 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
221
245
  any,
222
246
  Name
223
247
  >;
224
- pushPlannedWorkout: FunctionReference<
248
+ pushSchedule: FunctionReference<
225
249
  "action",
226
250
  "internal",
227
251
  {
228
252
  clientId: string;
229
253
  clientSecret: string;
254
+ date?: string;
230
255
  plannedWorkoutId: string;
231
256
  userId: string;
232
- workoutProvider?: string;
233
257
  },
234
258
  any,
235
259
  Name
236
260
  >;
237
- syncAllTypes: FunctionReference<
238
- "action",
239
- "internal",
240
- {
241
- accessToken: string;
242
- connectionId: string;
243
- uploadEndTimeInSeconds: number;
244
- uploadStartTimeInSeconds: number;
245
- userId: string;
246
- },
247
- any,
248
- Name
249
- >;
250
- syncGarmin: FunctionReference<
261
+ pushWorkout: FunctionReference<
251
262
  "action",
252
263
  "internal",
253
264
  {
254
265
  clientId: string;
255
266
  clientSecret: string;
256
- endTimeInSeconds?: number;
257
- startTimeInSeconds?: number;
267
+ plannedWorkoutId: string;
258
268
  userId: string;
269
+ workoutProvider?: string;
259
270
  },
260
271
  any,
261
272
  Name