@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.
- package/dist/client/garmin.d.ts +287 -0
- package/dist/client/garmin.d.ts.map +1 -0
- package/dist/client/garmin.js +345 -0
- package/dist/client/garmin.js.map +1 -0
- package/dist/client/index.d.ts +27 -467
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +33 -385
- package/dist/client/index.js.map +1 -1
- package/dist/client/strava.d.ts +92 -0
- package/dist/client/strava.d.ts.map +1 -0
- package/dist/client/strava.js +96 -0
- package/dist/client/strava.js.map +1 -0
- package/dist/client/types.d.ts +165 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +17 -12
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/public.d.ts +18 -84
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +147 -539
- package/dist/component/garmin/public.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +487 -0
- package/src/client/index.ts +69 -711
- package/src/client/strava.ts +108 -0
- package/src/client/types.ts +215 -18
- package/src/component/_generated/component.ts +29 -18
- package/src/component/garmin/public.ts +1049 -1406
- package/src/component/garmin/webhooks.ts +857 -857
|
@@ -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
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -1,18 +1,215 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
GenericActionCtx,
|
|
3
|
-
GenericMutationCtx,
|
|
4
|
-
GenericQueryCtx,
|
|
5
|
-
GenericDataModel,
|
|
6
|
-
} from "convex/server";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
startTimeInSeconds?: number;
|
|
267
|
+
plannedWorkoutId: string;
|
|
258
268
|
userId: string;
|
|
269
|
+
workoutProvider?: string;
|
|
259
270
|
},
|
|
260
271
|
any,
|
|
261
272
|
Name
|