@nativesquare/soma 0.11.0 → 0.13.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 +291 -0
- package/dist/client/garmin.d.ts.map +1 -0
- package/dist/client/garmin.js +493 -0
- package/dist/client/garmin.js.map +1 -0
- package/dist/client/index.d.ts +29 -394
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +30 -520
- package/dist/client/index.js.map +1 -1
- package/dist/client/strava.d.ts +97 -0
- package/dist/client/strava.d.ts.map +1 -0
- package/dist/client/strava.js +160 -0
- package/dist/client/strava.js.map +1 -0
- package/dist/client/types.d.ts +238 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +24 -12
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin/private.d.ts +53 -68
- package/dist/component/garmin/private.d.ts.map +1 -1
- package/dist/component/garmin/private.js +87 -85
- package/dist/component/garmin/private.js.map +1 -1
- package/dist/component/garmin/public.d.ts +97 -53
- package/dist/component/garmin/public.d.ts.map +1 -1
- package/dist/component/garmin/public.js +75 -148
- package/dist/component/garmin/public.js.map +1 -1
- package/dist/component/garmin/webhooks.d.ts +22 -20
- package/dist/component/garmin/webhooks.d.ts.map +1 -1
- package/dist/component/garmin/webhooks.js +115 -76
- package/dist/component/garmin/webhooks.js.map +1 -1
- package/dist/component/public.d.ts +15 -15
- package/dist/component/schema.d.ts +25 -25
- package/dist/component/strava/public.d.ts +12 -8
- package/dist/component/strava/public.d.ts.map +1 -1
- package/dist/component/strava/public.js +7 -7
- package/dist/component/strava/public.js.map +1 -1
- package/dist/component/validators/activity.d.ts +4 -4
- package/dist/component/validators/body.d.ts +4 -4
- package/dist/component/validators/daily.d.ts +4 -4
- package/dist/component/validators/nutrition.d.ts +3 -3
- package/dist/component/validators/samples.d.ts +4 -4
- package/dist/component/validators/shared.d.ts +13 -4
- package/dist/component/validators/shared.d.ts.map +1 -1
- package/dist/component/validators/shared.js +7 -0
- package/dist/component/validators/shared.js.map +1 -1
- package/dist/component/validators/sleep.d.ts +5 -5
- package/dist/validators.d.ts +41 -40
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +1 -0
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/client/garmin.ts +692 -0
- package/src/client/index.ts +68 -933
- package/src/client/strava.ts +199 -0
- package/src/client/types.ts +285 -0
- package/src/component/_generated/component.ts +19 -32
- package/src/component/garmin/private.ts +1872 -1870
- package/src/component/garmin/public.ts +1073 -1184
- package/src/component/garmin/webhooks.ts +898 -857
- package/src/component/strava/public.ts +393 -393
- package/src/component/validators/shared.ts +9 -0
- package/src/validators.ts +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { SomaComponent } from "./index.js";
|
|
2
|
+
import type { ActionCtx, SomaStravaConfig, RegisterRoutesOptions } from "./types.js";
|
|
3
|
+
import { httpActionGeneric, type HttpRouter } from "convex/server";
|
|
4
|
+
|
|
5
|
+
export const STRAVA_CALLBACK_PATH = "/api/strava/callback";
|
|
6
|
+
|
|
7
|
+
export class SomaStrava {
|
|
8
|
+
constructor(
|
|
9
|
+
private component: SomaComponent,
|
|
10
|
+
private requireConfig: () => SomaStravaConfig,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a Strava OAuth authorization URL.
|
|
15
|
+
*
|
|
16
|
+
* The state parameter is stored inside the component automatically,
|
|
17
|
+
* and the callback handler registered by `registerRoutes` will
|
|
18
|
+
* complete the flow without further host-app intervention.
|
|
19
|
+
*
|
|
20
|
+
* @param ctx - Action context from the host app
|
|
21
|
+
* @param opts.userId - The host app's user identifier
|
|
22
|
+
* @param opts.redirectUri - The URL Strava will redirect to after authorization
|
|
23
|
+
* @param opts.scope - Comma-separated Strava OAuth scopes (default: "read,activity:read_all,profile:read_all")
|
|
24
|
+
* @returns `{ authUrl, state }`
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const { authUrl } = await soma.strava.getAuthUrl(ctx, {
|
|
29
|
+
* userId: "user_123",
|
|
30
|
+
* redirectUri: "https://your-app.convex.site/api/strava/callback",
|
|
31
|
+
* });
|
|
32
|
+
* // Redirect user to authUrl — the callback is handled automatically
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
async getAuthUrl(
|
|
36
|
+
ctx: ActionCtx,
|
|
37
|
+
opts: { userId: string; redirectUri: string; scope?: string },
|
|
38
|
+
) {
|
|
39
|
+
const config = this.requireConfig();
|
|
40
|
+
return await ctx.runAction(this.component.strava.public.getStravaAuthUrl, {
|
|
41
|
+
clientId: config.clientId,
|
|
42
|
+
redirectUri: opts.redirectUri,
|
|
43
|
+
scope: opts.scope,
|
|
44
|
+
userId: opts.userId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sync activities from Strava for an already-connected user.
|
|
50
|
+
*
|
|
51
|
+
* Automatically refreshes the access token if expired. Fetches the
|
|
52
|
+
* athlete profile and activities, transforms them, and ingests into Soma.
|
|
53
|
+
*
|
|
54
|
+
* @param ctx - Action context from the host app
|
|
55
|
+
* @param args.userId - The host app's user identifier
|
|
56
|
+
* @param args.after - Only sync activities after this Unix epoch timestamp (for incremental sync)
|
|
57
|
+
* @returns `{ synced, errors }`
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* export const syncStrava = action({
|
|
62
|
+
* args: { userId: v.string() },
|
|
63
|
+
* handler: async (ctx, { userId }) => {
|
|
64
|
+
* return await soma.strava.sync(ctx, { userId });
|
|
65
|
+
* },
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
async sync(
|
|
70
|
+
ctx: ActionCtx,
|
|
71
|
+
args: { userId: string; after?: number },
|
|
72
|
+
) {
|
|
73
|
+
const config = this.requireConfig();
|
|
74
|
+
return await ctx.runAction(this.component.strava.public.syncStrava, {
|
|
75
|
+
...args,
|
|
76
|
+
clientId: config.clientId,
|
|
77
|
+
clientSecret: config.clientSecret,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Disconnect a user from Strava.
|
|
83
|
+
*
|
|
84
|
+
* Revokes the token at Strava (best-effort), deletes stored tokens,
|
|
85
|
+
* and sets the connection to inactive.
|
|
86
|
+
*
|
|
87
|
+
* @param ctx - Action context from the host app
|
|
88
|
+
* @param args.userId - The host app's user identifier
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* export const disconnectStrava = action({
|
|
93
|
+
* args: { userId: v.string() },
|
|
94
|
+
* handler: async (ctx, { userId }) => {
|
|
95
|
+
* await soma.strava.disconnect(ctx, { userId });
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
async disconnect(
|
|
101
|
+
ctx: ActionCtx,
|
|
102
|
+
args: { userId: string },
|
|
103
|
+
) {
|
|
104
|
+
const config = this.requireConfig();
|
|
105
|
+
return await ctx.runAction(this.component.strava.public.disconnectStrava, {
|
|
106
|
+
...args,
|
|
107
|
+
clientId: config.clientId,
|
|
108
|
+
clientSecret: config.clientSecret,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static registerRoutes(
|
|
113
|
+
http: HttpRouter,
|
|
114
|
+
component: SomaComponent,
|
|
115
|
+
opts?: RegisterRoutesOptions["strava"],
|
|
116
|
+
) {
|
|
117
|
+
const oauth = opts?.oauth ?? {};
|
|
118
|
+
const path = oauth.path ?? STRAVA_CALLBACK_PATH;
|
|
119
|
+
|
|
120
|
+
http.route({
|
|
121
|
+
path,
|
|
122
|
+
method: "GET",
|
|
123
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
124
|
+
const url = new URL(request.url);
|
|
125
|
+
const code = url.searchParams.get("code");
|
|
126
|
+
const state = url.searchParams.get("state");
|
|
127
|
+
|
|
128
|
+
if (!code) {
|
|
129
|
+
return new Response("Missing authorization code", { status: 400 });
|
|
130
|
+
}
|
|
131
|
+
if (!state) {
|
|
132
|
+
return new Response(
|
|
133
|
+
"Missing state parameter. Ensure the state was included " +
|
|
134
|
+
"when building the Strava auth URL via getStravaAuthUrl.",
|
|
135
|
+
{ status: 400 },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const clientId =
|
|
140
|
+
opts?.clientId ?? process.env.STRAVA_CLIENT_ID;
|
|
141
|
+
const clientSecret =
|
|
142
|
+
opts?.clientSecret ?? process.env.STRAVA_CLIENT_SECRET;
|
|
143
|
+
|
|
144
|
+
if (!clientId || !clientSecret) {
|
|
145
|
+
return new Response(
|
|
146
|
+
"Strava credentials not configured. Set STRAVA_CLIENT_ID and " +
|
|
147
|
+
"STRAVA_CLIENT_SECRET environment variables, or pass them to registerRoutes.",
|
|
148
|
+
{ status: 500 },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let result: {
|
|
153
|
+
connectionId: string;
|
|
154
|
+
userId: string;
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
result = await ctx.runAction(component.strava.public.completeStravaOAuth, {
|
|
158
|
+
code,
|
|
159
|
+
state,
|
|
160
|
+
clientId,
|
|
161
|
+
clientSecret,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const message =
|
|
165
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
166
|
+
return new Response(`Strava OAuth callback failed: ${message}`, {
|
|
167
|
+
status: 500,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (oauth.onComplete) {
|
|
172
|
+
try {
|
|
173
|
+
await oauth.onComplete(ctx, {
|
|
174
|
+
provider: "STRAVA",
|
|
175
|
+
userId: result.userId,
|
|
176
|
+
connectionId: result.connectionId,
|
|
177
|
+
});
|
|
178
|
+
} catch (callbackError) {
|
|
179
|
+
console.error(
|
|
180
|
+
"[soma] strava onComplete callback error:",
|
|
181
|
+
callbackError instanceof Error ? callbackError.message : callbackError,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (oauth.redirectTo) {
|
|
187
|
+
return new Response(null, {
|
|
188
|
+
status: 302,
|
|
189
|
+
headers: { Location: oauth.redirectTo },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return new Response("Successfully connected to Strava!", {
|
|
194
|
+
status: 200,
|
|
195
|
+
});
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -5,6 +5,10 @@ import type {
|
|
|
5
5
|
GenericDataModel,
|
|
6
6
|
} from "convex/server";
|
|
7
7
|
|
|
8
|
+
// ─── Context Types ──────────────────────────────────────────────────────────
|
|
9
|
+
// Narrowed Convex context types that only expose the runner methods each
|
|
10
|
+
// operation actually needs.
|
|
11
|
+
|
|
8
12
|
export type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
9
13
|
|
|
10
14
|
export type MutationCtx = Pick<
|
|
@@ -16,3 +20,284 @@ export type ActionCtx = Pick<
|
|
|
16
20
|
GenericActionCtx<GenericDataModel>,
|
|
17
21
|
"runQuery" | "runMutation" | "runAction"
|
|
18
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
|
+
// ─── Shared Error & Result Types ───────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A structured error from a Soma operation.
|
|
58
|
+
*
|
|
59
|
+
* Operational errors (API failures, transform errors, ingestion failures)
|
|
60
|
+
* are returned in the result — not thrown. Only configuration errors
|
|
61
|
+
* (missing connection, nonexistent document) are thrown as exceptions.
|
|
62
|
+
*/
|
|
63
|
+
export interface SomaError {
|
|
64
|
+
/** Category of the failed item (e.g. `"activity"`, `"pushWorkout"`, `"ingest"`). */
|
|
65
|
+
type: string;
|
|
66
|
+
/** Identifier of the failed item, or `"fetch"` for API-level failures. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Human-readable error description. */
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Standard result wrapper for all Soma data operations.
|
|
74
|
+
*
|
|
75
|
+
* - `data` contains the operation's success payload.
|
|
76
|
+
* - `errors` contains any operational failures that occurred.
|
|
77
|
+
*
|
|
78
|
+
* For **batch operations** (pull, sync), partial success is possible:
|
|
79
|
+
* `data` contains the counts of successfully processed items, while
|
|
80
|
+
* `errors` lists the individual failures.
|
|
81
|
+
*
|
|
82
|
+
* For **atomic operations** (push, delete), `data` is `null` when the
|
|
83
|
+
* operation fails, with a single entry in `errors` describing the cause.
|
|
84
|
+
*/
|
|
85
|
+
export type SomaResult<T> = {
|
|
86
|
+
data: T;
|
|
87
|
+
errors: SomaError[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─── Data Query & Ingestion Args ────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Common args shape for all ingestion methods.
|
|
94
|
+
*
|
|
95
|
+
* Requires `connectionId` and `userId` at minimum — additional fields
|
|
96
|
+
* come from the transformer output (e.g., `metadata`, `calories_data`, etc.)
|
|
97
|
+
* and are validated server-side by Convex validators.
|
|
98
|
+
*/
|
|
99
|
+
export type IngestArgs = {
|
|
100
|
+
connectionId: string;
|
|
101
|
+
userId: string;
|
|
102
|
+
} & Record<string, unknown>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Base args for time-range filtered queries.
|
|
106
|
+
*
|
|
107
|
+
* - `userId` is required for all health data queries.
|
|
108
|
+
* - `startTime` / `endTime` are optional ISO-8601 bounds on `metadata.start_time`.
|
|
109
|
+
*/
|
|
110
|
+
export type TimeRangeArgs = {
|
|
111
|
+
userId: string;
|
|
112
|
+
startTime?: string;
|
|
113
|
+
endTime?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Args for list (collect-all) queries with optional ordering and limit.
|
|
118
|
+
*/
|
|
119
|
+
export type ListTimeRangeArgs = TimeRangeArgs & {
|
|
120
|
+
order?: "asc" | "desc";
|
|
121
|
+
limit?: number;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Args for paginated queries with Convex pagination options.
|
|
126
|
+
*/
|
|
127
|
+
export type PaginateTimeRangeArgs = TimeRangeArgs & {
|
|
128
|
+
paginationOpts: { numItems: number; cursor: string | null };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ─── OAuth Callback Events ──────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/** Data passed to `onComplete` after Strava OAuth completes. */
|
|
134
|
+
export interface StravaConnectEvent {
|
|
135
|
+
provider: "STRAVA";
|
|
136
|
+
userId: string;
|
|
137
|
+
connectionId: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Data passed to `oauth.onComplete` after Garmin OAuth completes. */
|
|
141
|
+
export interface GarminConnectEvent {
|
|
142
|
+
provider: "GARMIN";
|
|
143
|
+
userId: string;
|
|
144
|
+
connectionId: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Garmin Webhook Types ───────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/** Args accepted by all Garmin webhook handler actions inside the component. */
|
|
150
|
+
export type GarminWebhookActionArgs = {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
payload: any;
|
|
153
|
+
autoIngest?: boolean;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Result returned by all Garmin webhook handler actions inside the component. */
|
|
157
|
+
export interface GarminWebhookActionResult {
|
|
158
|
+
errors: SomaError[];
|
|
159
|
+
items: GarminWebhookItem[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A single transformed item with its user/connection mapping. */
|
|
163
|
+
export interface GarminWebhookItem {
|
|
164
|
+
/** The Soma connection ID that this data belongs to. */
|
|
165
|
+
connectionId: string;
|
|
166
|
+
/** The host app's user ID. */
|
|
167
|
+
userId: string;
|
|
168
|
+
/** The transformed data in Soma's normalized format. */
|
|
169
|
+
data: Record<string, unknown>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Data passed to webhook `events` handlers and `onEvent` after processing.
|
|
174
|
+
*
|
|
175
|
+
* The host app receives the full set of transformed items plus the raw Garmin
|
|
176
|
+
* payload, regardless of the `autoIngest` setting.
|
|
177
|
+
*/
|
|
178
|
+
export interface GarminWebhookEvent {
|
|
179
|
+
/** The Garmin data type that triggered this webhook (e.g. `"activities"`, `"sleeps"`). */
|
|
180
|
+
dataType: string;
|
|
181
|
+
/** Errors encountered during connection resolution, transformation, or ingestion. */
|
|
182
|
+
errors: SomaError[];
|
|
183
|
+
/** The raw JSON payload received from Garmin, before any transformation. */
|
|
184
|
+
rawPayload: unknown;
|
|
185
|
+
/**
|
|
186
|
+
* Transformed items in Soma's normalized format.
|
|
187
|
+
*
|
|
188
|
+
* Always contains the full set of successfully transformed items, regardless
|
|
189
|
+
* of the `autoIngest` setting. When `autoIngest` is `true`, these are the
|
|
190
|
+
* same items that were written to the database. When `autoIngest` is `false`,
|
|
191
|
+
* the host app can use them for custom ingestion or processing.
|
|
192
|
+
*
|
|
193
|
+
* Empty for ping-mode webhooks (which contain no data).
|
|
194
|
+
*/
|
|
195
|
+
items: GarminWebhookItem[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Webhook endpoint names matching the Garmin API data types. */
|
|
199
|
+
export type GarminWebhookEventName =
|
|
200
|
+
| "activities" | "activity-details" | "manually-updated-activities" | "move-iq"
|
|
201
|
+
| "blood-pressures" | "body-compositions" | "dailies" | "epochs"
|
|
202
|
+
| "health-snapshot" | "sleeps" | "hrv" | "stress" | "pulse-ox"
|
|
203
|
+
| "respiration" | "skin-temp" | "user-metrics" | "menstrual-cycle-tracking";
|
|
204
|
+
|
|
205
|
+
/** Handler for a specific webhook event or the catch-all `onEvent`. */
|
|
206
|
+
export type GarminWebhookHandler = (
|
|
207
|
+
ctx: GenericActionCtx<GenericDataModel>,
|
|
208
|
+
event: GarminWebhookEvent,
|
|
209
|
+
) => Promise<void>;
|
|
210
|
+
|
|
211
|
+
// ─── Route Registration Options ─────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Per-provider options for `registerRoutes`.
|
|
215
|
+
*/
|
|
216
|
+
export interface StravaOAuthOptions {
|
|
217
|
+
/** HTTP path for the OAuth callback. @default "/api/strava/callback" */
|
|
218
|
+
path?: string;
|
|
219
|
+
/** URL to redirect the user to after a successful connection. */
|
|
220
|
+
redirectTo?: string;
|
|
221
|
+
/** Called after Strava OAuth completes and the connection is established. */
|
|
222
|
+
onComplete?: (
|
|
223
|
+
ctx: GenericActionCtx<GenericDataModel>,
|
|
224
|
+
event: StravaConnectEvent,
|
|
225
|
+
) => Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface GarminOAuthOptions {
|
|
229
|
+
/** HTTP path for the OAuth callback. @default "/api/garmin/callback" */
|
|
230
|
+
path?: string;
|
|
231
|
+
/** URL to redirect the user to after a successful connection. */
|
|
232
|
+
redirectTo?: string;
|
|
233
|
+
/** Called after Garmin OAuth completes and the connection is established. */
|
|
234
|
+
onComplete?: (
|
|
235
|
+
ctx: GenericActionCtx<GenericDataModel>,
|
|
236
|
+
event: GarminConnectEvent,
|
|
237
|
+
) => Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface GarminWebhookOptions {
|
|
241
|
+
/** Base path prefix for all webhook routes. @default "/api/garmin/webhook" */
|
|
242
|
+
basePath?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Whether to automatically ingest (upsert) transformed data into the Soma
|
|
245
|
+
* database when a webhook payload is received.
|
|
246
|
+
*
|
|
247
|
+
* When `true` (default), incoming data is validated, transformed, and written
|
|
248
|
+
* to the database automatically. When `false`, the webhook still receives and
|
|
249
|
+
* validates the payload, but skips the database write — useful when you want
|
|
250
|
+
* to handle ingestion yourself via the `onEvent` / per-type callbacks.
|
|
251
|
+
*
|
|
252
|
+
* @default true
|
|
253
|
+
*/
|
|
254
|
+
autoIngest?: boolean;
|
|
255
|
+
/** Called after every webhook payload is processed, regardless of data type. */
|
|
256
|
+
onEvent?: GarminWebhookHandler;
|
|
257
|
+
/**
|
|
258
|
+
* Per-data-type webhook registration.
|
|
259
|
+
*
|
|
260
|
+
* **Only data types listed here get an HTTP route registered.**
|
|
261
|
+
* Unlisted types are ignored — Garmin receives a 404 if it POSTs to them.
|
|
262
|
+
*
|
|
263
|
+
* Pass a handler function to run custom logic after ingestion,
|
|
264
|
+
* or `true` to register the route with default processing only.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* events: {
|
|
269
|
+
* "activities": async (ctx, event) => { // custom side-effect },
|
|
270
|
+
* "sleeps": true, // register route, default processing only
|
|
271
|
+
* "dailies": true,
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
events?: Partial<Record<GarminWebhookEventName, GarminWebhookHandler | true>>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface RegisterRoutesOptions {
|
|
279
|
+
strava?: {
|
|
280
|
+
/** Override STRAVA_CLIENT_ID env var. */
|
|
281
|
+
clientId?: string;
|
|
282
|
+
/** Override STRAVA_CLIENT_SECRET env var. */
|
|
283
|
+
clientSecret?: string;
|
|
284
|
+
/** OAuth callback configuration. */
|
|
285
|
+
oauth?: StravaOAuthOptions;
|
|
286
|
+
};
|
|
287
|
+
garmin?: {
|
|
288
|
+
/** Override GARMIN_CLIENT_ID env var. */
|
|
289
|
+
clientId?: string;
|
|
290
|
+
/** Override GARMIN_CLIENT_SECRET env var. */
|
|
291
|
+
clientSecret?: string;
|
|
292
|
+
/** OAuth callback configuration. */
|
|
293
|
+
oauth?: GarminOAuthOptions;
|
|
294
|
+
/**
|
|
295
|
+
* Webhook route configuration.
|
|
296
|
+
*
|
|
297
|
+
* Routes are **disabled by default**. Only data types listed in `events`
|
|
298
|
+
* get an HTTP endpoint registered. Omit entirely or set to `false` to
|
|
299
|
+
* skip all webhook routes.
|
|
300
|
+
*/
|
|
301
|
+
webhook?: GarminWebhookOptions | false;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -245,19 +245,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
245
245
|
any,
|
|
246
246
|
Name
|
|
247
247
|
>;
|
|
248
|
-
pushPlannedWorkout: FunctionReference<
|
|
249
|
-
"action",
|
|
250
|
-
"internal",
|
|
251
|
-
{
|
|
252
|
-
clientId: string;
|
|
253
|
-
clientSecret: string;
|
|
254
|
-
plannedWorkoutId: string;
|
|
255
|
-
userId: string;
|
|
256
|
-
workoutProvider?: string;
|
|
257
|
-
},
|
|
258
|
-
any,
|
|
259
|
-
Name
|
|
260
|
-
>;
|
|
261
248
|
pushSchedule: FunctionReference<
|
|
262
249
|
"action",
|
|
263
250
|
"internal",
|
|
@@ -289,119 +276,119 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
289
276
|
handleGarminWebhookActivities: FunctionReference<
|
|
290
277
|
"action",
|
|
291
278
|
"internal",
|
|
292
|
-
{ payload: any },
|
|
279
|
+
{ autoIngest?: boolean; payload: any },
|
|
293
280
|
any,
|
|
294
281
|
Name
|
|
295
282
|
>;
|
|
296
283
|
handleGarminWebhookActivityDetails: FunctionReference<
|
|
297
284
|
"action",
|
|
298
285
|
"internal",
|
|
299
|
-
{ payload: any },
|
|
286
|
+
{ autoIngest?: boolean; payload: any },
|
|
300
287
|
any,
|
|
301
288
|
Name
|
|
302
289
|
>;
|
|
303
290
|
handleGarminWebhookBloodPressures: FunctionReference<
|
|
304
291
|
"action",
|
|
305
292
|
"internal",
|
|
306
|
-
{ payload: any },
|
|
293
|
+
{ autoIngest?: boolean; payload: any },
|
|
307
294
|
any,
|
|
308
295
|
Name
|
|
309
296
|
>;
|
|
310
297
|
handleGarminWebhookBodyCompositions: FunctionReference<
|
|
311
298
|
"action",
|
|
312
299
|
"internal",
|
|
313
|
-
{ payload: any },
|
|
300
|
+
{ autoIngest?: boolean; payload: any },
|
|
314
301
|
any,
|
|
315
302
|
Name
|
|
316
303
|
>;
|
|
317
304
|
handleGarminWebhookDailies: FunctionReference<
|
|
318
305
|
"action",
|
|
319
306
|
"internal",
|
|
320
|
-
{ payload: any },
|
|
307
|
+
{ autoIngest?: boolean; payload: any },
|
|
321
308
|
any,
|
|
322
309
|
Name
|
|
323
310
|
>;
|
|
324
311
|
handleGarminWebhookEpochs: FunctionReference<
|
|
325
312
|
"action",
|
|
326
313
|
"internal",
|
|
327
|
-
{ payload: any },
|
|
314
|
+
{ autoIngest?: boolean; payload: any },
|
|
328
315
|
any,
|
|
329
316
|
Name
|
|
330
317
|
>;
|
|
331
318
|
handleGarminWebhookHealthSnapshot: FunctionReference<
|
|
332
319
|
"action",
|
|
333
320
|
"internal",
|
|
334
|
-
{ payload: any },
|
|
321
|
+
{ autoIngest?: boolean; payload: any },
|
|
335
322
|
any,
|
|
336
323
|
Name
|
|
337
324
|
>;
|
|
338
325
|
handleGarminWebhookHRVSummary: FunctionReference<
|
|
339
326
|
"action",
|
|
340
327
|
"internal",
|
|
341
|
-
{ payload: any },
|
|
328
|
+
{ autoIngest?: boolean; payload: any },
|
|
342
329
|
any,
|
|
343
330
|
Name
|
|
344
331
|
>;
|
|
345
332
|
handleGarminWebhookManuallyUpdatedActivities: FunctionReference<
|
|
346
333
|
"action",
|
|
347
334
|
"internal",
|
|
348
|
-
{ payload: any },
|
|
335
|
+
{ autoIngest?: boolean; payload: any },
|
|
349
336
|
any,
|
|
350
337
|
Name
|
|
351
338
|
>;
|
|
352
339
|
handleGarminWebhookMenstrualCycleTracking: FunctionReference<
|
|
353
340
|
"action",
|
|
354
341
|
"internal",
|
|
355
|
-
{ payload: any },
|
|
342
|
+
{ autoIngest?: boolean; payload: any },
|
|
356
343
|
any,
|
|
357
344
|
Name
|
|
358
345
|
>;
|
|
359
346
|
handleGarminWebhookMoveIQ: FunctionReference<
|
|
360
347
|
"action",
|
|
361
348
|
"internal",
|
|
362
|
-
{ payload: any },
|
|
349
|
+
{ autoIngest?: boolean; payload: any },
|
|
363
350
|
any,
|
|
364
351
|
Name
|
|
365
352
|
>;
|
|
366
353
|
handleGarminWebhookPulseOx: FunctionReference<
|
|
367
354
|
"action",
|
|
368
355
|
"internal",
|
|
369
|
-
{ payload: any },
|
|
356
|
+
{ autoIngest?: boolean; payload: any },
|
|
370
357
|
any,
|
|
371
358
|
Name
|
|
372
359
|
>;
|
|
373
360
|
handleGarminWebhookRespiration: FunctionReference<
|
|
374
361
|
"action",
|
|
375
362
|
"internal",
|
|
376
|
-
{ payload: any },
|
|
363
|
+
{ autoIngest?: boolean; payload: any },
|
|
377
364
|
any,
|
|
378
365
|
Name
|
|
379
366
|
>;
|
|
380
367
|
handleGarminWebhookSkinTemp: FunctionReference<
|
|
381
368
|
"action",
|
|
382
369
|
"internal",
|
|
383
|
-
{ payload: any },
|
|
370
|
+
{ autoIngest?: boolean; payload: any },
|
|
384
371
|
any,
|
|
385
372
|
Name
|
|
386
373
|
>;
|
|
387
374
|
handleGarminWebhookSleeps: FunctionReference<
|
|
388
375
|
"action",
|
|
389
376
|
"internal",
|
|
390
|
-
{ payload: any },
|
|
377
|
+
{ autoIngest?: boolean; payload: any },
|
|
391
378
|
any,
|
|
392
379
|
Name
|
|
393
380
|
>;
|
|
394
381
|
handleGarminWebhookStress: FunctionReference<
|
|
395
382
|
"action",
|
|
396
383
|
"internal",
|
|
397
|
-
{ payload: any },
|
|
384
|
+
{ autoIngest?: boolean; payload: any },
|
|
398
385
|
any,
|
|
399
386
|
Name
|
|
400
387
|
>;
|
|
401
388
|
handleGarminWebhookUserMetrics: FunctionReference<
|
|
402
389
|
"action",
|
|
403
390
|
"internal",
|
|
404
|
-
{ payload: any },
|
|
391
|
+
{ autoIngest?: boolean; payload: any },
|
|
405
392
|
any,
|
|
406
393
|
Name
|
|
407
394
|
>;
|
|
@@ -1880,8 +1867,8 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1880
1867
|
userId: string;
|
|
1881
1868
|
},
|
|
1882
1869
|
{
|
|
1883
|
-
|
|
1884
|
-
|
|
1870
|
+
data: { synced: { activities: number; athletes: number } };
|
|
1871
|
+
errors: Array<{ id: string; message: string; type: string }>;
|
|
1885
1872
|
},
|
|
1886
1873
|
Name
|
|
1887
1874
|
>;
|