@nativesquare/soma 0.3.0 → 0.4.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/index.d.ts +167 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +150 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -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 +56 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +110 -0
- package/dist/component/garmin.d.ts.map +1 -0
- package/dist/component/garmin.js +454 -0
- package/dist/component/garmin.js.map +1 -0
- package/dist/component/public.d.ts +761 -761
- package/dist/component/schema.d.ts +390 -388
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -2
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +5 -4
- package/dist/component/strava.d.ts.map +1 -1
- package/dist/component/strava.js +18 -1
- package/dist/component/strava.js.map +1 -1
- package/dist/component/validators/activity.d.ts +42 -42
- package/dist/component/validators/body.d.ts +47 -47
- package/dist/component/validators/daily.d.ts +17 -17
- package/dist/component/validators/plannedWorkout.d.ts +5 -5
- package/dist/component/validators/samples.d.ts +2 -2
- package/dist/component/validators/shared.d.ts +17 -17
- package/dist/component/validators/sleep.d.ts +17 -17
- package/dist/garmin/activity.d.ts +101 -0
- package/dist/garmin/activity.d.ts.map +1 -0
- package/dist/garmin/activity.js +207 -0
- package/dist/garmin/activity.js.map +1 -0
- package/dist/garmin/auth.d.ts +65 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +155 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/body.d.ts +26 -0
- package/dist/garmin/body.d.ts.map +1 -0
- package/dist/garmin/body.js +44 -0
- package/dist/garmin/body.js.map +1 -0
- package/dist/garmin/client.d.ts +99 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +153 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +74 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +143 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/index.d.ts +20 -0
- package/dist/garmin/index.d.ts.map +1 -0
- package/dist/garmin/index.js +21 -0
- package/dist/garmin/index.js.map +1 -0
- package/dist/garmin/maps/activity-type.d.ts +7 -0
- package/dist/garmin/maps/activity-type.d.ts.map +1 -0
- package/dist/garmin/maps/activity-type.js +98 -0
- package/dist/garmin/maps/activity-type.js.map +1 -0
- package/dist/garmin/maps/sleep-level.d.ts +6 -0
- package/dist/garmin/maps/sleep-level.d.ts.map +1 -0
- package/dist/garmin/maps/sleep-level.js +21 -0
- package/dist/garmin/maps/sleep-level.js.map +1 -0
- package/dist/garmin/menstruation.d.ts +23 -0
- package/dist/garmin/menstruation.d.ts.map +1 -0
- package/dist/garmin/menstruation.js +34 -0
- package/dist/garmin/menstruation.js.map +1 -0
- package/dist/garmin/sleep.d.ts +62 -0
- package/dist/garmin/sleep.d.ts.map +1 -0
- package/dist/garmin/sleep.js +125 -0
- package/dist/garmin/sleep.js.map +1 -0
- package/dist/garmin/sync.d.ts +39 -0
- package/dist/garmin/sync.d.ts.map +1 -0
- package/dist/garmin/sync.js +175 -0
- package/dist/garmin/sync.js.map +1 -0
- package/dist/garmin/types.d.ts +212 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +8 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/validators.d.ts +331 -331
- package/package.json +5 -1
- package/src/client/index.ts +194 -1
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +62 -0
- package/src/component/garmin.ts +534 -0
- package/src/component/schema.ts +3 -2
- package/src/component/strava.ts +23 -1
- package/src/garmin/activity.test.ts +178 -0
- package/src/garmin/activity.ts +272 -0
- package/src/garmin/auth.test.ts +128 -0
- package/src/garmin/auth.ts +249 -0
- package/src/garmin/body.ts +59 -0
- package/src/garmin/client.ts +254 -0
- package/src/garmin/daily.ts +211 -0
- package/src/garmin/index.ts +76 -0
- package/src/garmin/maps/activity-type.test.ts +78 -0
- package/src/garmin/maps/activity-type.ts +116 -0
- package/src/garmin/maps/sleep-level.ts +22 -0
- package/src/garmin/menstruation.ts +42 -0
- package/src/garmin/sleep.test.ts +110 -0
- package/src/garmin/sleep.ts +170 -0
- package/src/garmin/sync.ts +223 -0
- package/src/garmin/types.ts +338 -0
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/NativeSquare/soma/issues"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"convex",
|
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
"types": "./dist/strava/index.d.ts",
|
|
58
58
|
"default": "./dist/strava/index.js"
|
|
59
59
|
},
|
|
60
|
+
"./garmin": {
|
|
61
|
+
"types": "./dist/garmin/index.d.ts",
|
|
62
|
+
"default": "./dist/garmin/index.js"
|
|
63
|
+
},
|
|
60
64
|
"./validators": {
|
|
61
65
|
"types": "./dist/validators.d.ts",
|
|
62
66
|
"default": "./dist/validators.js"
|
package/src/client/index.ts
CHANGED
|
@@ -24,6 +24,20 @@ export interface SomaStravaConfig {
|
|
|
24
24
|
baseUrl?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Configuration for the Garmin integration.
|
|
29
|
+
*
|
|
30
|
+
* If not provided to the constructor, the Soma class will attempt to
|
|
31
|
+
* read `GARMIN_CONSUMER_KEY` and `GARMIN_CONSUMER_SECRET` from
|
|
32
|
+
* environment variables automatically.
|
|
33
|
+
*/
|
|
34
|
+
export interface SomaGarminConfig {
|
|
35
|
+
/** Your Garmin application's Consumer Key. */
|
|
36
|
+
consumerKey: string;
|
|
37
|
+
/** Your Garmin application's Consumer Secret. */
|
|
38
|
+
consumerSecret: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Client class for the @nativesquare/soma Convex component.
|
|
29
43
|
*
|
|
@@ -60,12 +74,14 @@ export interface SomaStravaConfig {
|
|
|
60
74
|
*/
|
|
61
75
|
export class Soma {
|
|
62
76
|
private stravaConfig?: SomaStravaConfig;
|
|
77
|
+
private garminConfig?: SomaGarminConfig;
|
|
63
78
|
|
|
64
79
|
constructor(
|
|
65
80
|
public component: SomaComponent,
|
|
66
|
-
options?: { strava?: SomaStravaConfig },
|
|
81
|
+
options?: { strava?: SomaStravaConfig; garmin?: SomaGarminConfig },
|
|
67
82
|
) {
|
|
68
83
|
this.stravaConfig = options?.strava ?? this.readStravaEnv();
|
|
84
|
+
this.garminConfig = options?.garmin ?? this.readGarminEnv();
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
/**
|
|
@@ -97,6 +113,31 @@ export class Soma {
|
|
|
97
113
|
return this.stravaConfig;
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Read Garmin config from environment variables.
|
|
118
|
+
* Returns undefined if the required vars are not set.
|
|
119
|
+
*/
|
|
120
|
+
private readGarminEnv(): SomaGarminConfig | undefined {
|
|
121
|
+
const consumerKey = process.env.GARMIN_CONSUMER_KEY;
|
|
122
|
+
const consumerSecret = process.env.GARMIN_CONSUMER_SECRET;
|
|
123
|
+
if (!consumerKey || !consumerSecret) return undefined;
|
|
124
|
+
return { consumerKey, consumerSecret };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the resolved Garmin config, or throw a clear error if not configured.
|
|
129
|
+
*/
|
|
130
|
+
private requireGarminConfig(): SomaGarminConfig {
|
|
131
|
+
if (!this.garminConfig) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Garmin is not configured. Either set GARMIN_CONSUMER_KEY and " +
|
|
134
|
+
"GARMIN_CONSUMER_SECRET environment variables in the Convex dashboard, " +
|
|
135
|
+
"or pass { garmin: { consumerKey, consumerSecret } } to the Soma constructor.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return this.garminConfig;
|
|
139
|
+
}
|
|
140
|
+
|
|
100
141
|
// ─── Connect / Disconnect ───────────────────────────────────────────────────
|
|
101
142
|
|
|
102
143
|
/**
|
|
@@ -820,6 +861,158 @@ export class Soma {
|
|
|
820
861
|
baseUrl: config.baseUrl,
|
|
821
862
|
});
|
|
822
863
|
}
|
|
864
|
+
|
|
865
|
+
// ─── Garmin Integration ──────────────────────────────────────────────────────
|
|
866
|
+
// High-level methods that handle OAuth 1.0a, token storage, and data syncing
|
|
867
|
+
// for Garmin. Requires Garmin credentials to be configured either via
|
|
868
|
+
// environment variables or the constructor.
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Step 1 of the Garmin OAuth 1.0a flow: obtain a request token.
|
|
872
|
+
*
|
|
873
|
+
* Returns the temporary `token`, `tokenSecret`, and the `authUrl` to
|
|
874
|
+
* redirect the user to. The host app must store `token` and `tokenSecret`
|
|
875
|
+
* temporarily (e.g., in session/cookie) and pass them to `connectGarmin`
|
|
876
|
+
* after the user authorizes.
|
|
877
|
+
*
|
|
878
|
+
* @param ctx - Action context from the host app
|
|
879
|
+
* @param opts.callbackUrl - The URL Garmin will redirect to after authorization
|
|
880
|
+
* @returns `{ token, tokenSecret, authUrl }`
|
|
881
|
+
*
|
|
882
|
+
* @example
|
|
883
|
+
* ```ts
|
|
884
|
+
* const { token, tokenSecret, authUrl } = await soma.getGarminRequestToken(ctx, {
|
|
885
|
+
* callbackUrl: "https://your-app.com/api/garmin/callback",
|
|
886
|
+
* });
|
|
887
|
+
* // Store token + tokenSecret in session, redirect user to authUrl
|
|
888
|
+
* ```
|
|
889
|
+
*/
|
|
890
|
+
async getGarminRequestToken(
|
|
891
|
+
ctx: ActionCtx,
|
|
892
|
+
opts: { callbackUrl?: string },
|
|
893
|
+
) {
|
|
894
|
+
const config = this.requireGarminConfig();
|
|
895
|
+
return await ctx.runAction(this.component.garmin.getGarminRequestToken, {
|
|
896
|
+
consumerKey: config.consumerKey,
|
|
897
|
+
consumerSecret: config.consumerSecret,
|
|
898
|
+
callbackUrl: opts.callbackUrl,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Step 3 of the Garmin OAuth 1.0a flow + initial data sync.
|
|
904
|
+
*
|
|
905
|
+
* Exchanges the request token + verifier for permanent access tokens,
|
|
906
|
+
* creates/reactivates the Soma connection, stores tokens securely,
|
|
907
|
+
* and syncs the last 30 days of all data types (activities, dailies,
|
|
908
|
+
* sleep, body composition, menstruation).
|
|
909
|
+
*
|
|
910
|
+
* Call this from your OAuth callback endpoint after receiving the
|
|
911
|
+
* `oauth_verifier` query parameter from Garmin.
|
|
912
|
+
*
|
|
913
|
+
* @param ctx - Action context from the host app
|
|
914
|
+
* @param args.userId - The host app's user identifier
|
|
915
|
+
* @param args.token - The request token from Step 1
|
|
916
|
+
* @param args.tokenSecret - The request token secret from Step 1
|
|
917
|
+
* @param args.verifier - The oauth_verifier from the callback
|
|
918
|
+
* @returns `{ connectionId, synced, errors }`
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```ts
|
|
922
|
+
* export const handleGarminCallback = action({
|
|
923
|
+
* args: {
|
|
924
|
+
* userId: v.string(),
|
|
925
|
+
* token: v.string(),
|
|
926
|
+
* tokenSecret: v.string(),
|
|
927
|
+
* verifier: v.string(),
|
|
928
|
+
* },
|
|
929
|
+
* handler: async (ctx, args) => {
|
|
930
|
+
* return await soma.connectGarmin(ctx, args);
|
|
931
|
+
* },
|
|
932
|
+
* });
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
async connectGarmin(
|
|
936
|
+
ctx: ActionCtx,
|
|
937
|
+
args: {
|
|
938
|
+
userId: string;
|
|
939
|
+
token: string;
|
|
940
|
+
tokenSecret: string;
|
|
941
|
+
verifier: string;
|
|
942
|
+
},
|
|
943
|
+
) {
|
|
944
|
+
const config = this.requireGarminConfig();
|
|
945
|
+
return await ctx.runAction(this.component.garmin.connectGarmin, {
|
|
946
|
+
...args,
|
|
947
|
+
consumerKey: config.consumerKey,
|
|
948
|
+
consumerSecret: config.consumerSecret,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Sync all data types from Garmin for an already-connected user.
|
|
954
|
+
*
|
|
955
|
+
* Fetches activities, dailies, sleep, body composition, and menstruation
|
|
956
|
+
* data for the specified time range (defaults to last 30 days).
|
|
957
|
+
*
|
|
958
|
+
* @param ctx - Action context from the host app
|
|
959
|
+
* @param args.userId - The host app's user identifier
|
|
960
|
+
* @param args.startTimeInSeconds - Optional start of time range (Unix epoch seconds)
|
|
961
|
+
* @param args.endTimeInSeconds - Optional end of time range (Unix epoch seconds)
|
|
962
|
+
* @returns `{ synced, errors }`
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* ```ts
|
|
966
|
+
* export const syncGarmin = action({
|
|
967
|
+
* args: { userId: v.string() },
|
|
968
|
+
* handler: async (ctx, { userId }) => {
|
|
969
|
+
* return await soma.syncGarmin(ctx, { userId });
|
|
970
|
+
* },
|
|
971
|
+
* });
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
async syncGarmin(
|
|
975
|
+
ctx: ActionCtx,
|
|
976
|
+
args: {
|
|
977
|
+
userId: string;
|
|
978
|
+
startTimeInSeconds?: number;
|
|
979
|
+
endTimeInSeconds?: number;
|
|
980
|
+
},
|
|
981
|
+
) {
|
|
982
|
+
const config = this.requireGarminConfig();
|
|
983
|
+
return await ctx.runAction(this.component.garmin.syncGarmin, {
|
|
984
|
+
...args,
|
|
985
|
+
consumerKey: config.consumerKey,
|
|
986
|
+
consumerSecret: config.consumerSecret,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Disconnect a user from Garmin.
|
|
992
|
+
*
|
|
993
|
+
* Deletes stored tokens and sets the connection to inactive.
|
|
994
|
+
* Garmin OAuth 1.0a tokens cannot be revoked via API, so cleanup
|
|
995
|
+
* is local only.
|
|
996
|
+
*
|
|
997
|
+
* @param ctx - Action context from the host app
|
|
998
|
+
* @param args.userId - The host app's user identifier
|
|
999
|
+
*
|
|
1000
|
+
* @example
|
|
1001
|
+
* ```ts
|
|
1002
|
+
* export const disconnectGarmin = action({
|
|
1003
|
+
* args: { userId: v.string() },
|
|
1004
|
+
* handler: async (ctx, { userId }) => {
|
|
1005
|
+
* await soma.disconnectGarmin(ctx, { userId });
|
|
1006
|
+
* },
|
|
1007
|
+
* });
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
async disconnectGarmin(
|
|
1011
|
+
ctx: ActionCtx,
|
|
1012
|
+
args: { userId: string },
|
|
1013
|
+
) {
|
|
1014
|
+
return await ctx.runAction(this.component.garmin.disconnectGarmin, args);
|
|
1015
|
+
}
|
|
823
1016
|
}
|
|
824
1017
|
|
|
825
1018
|
// ─── Shared Types ────────────────────────────────────────────────────────────
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type * as garmin from "../garmin.js";
|
|
11
12
|
import type * as private_ from "../private.js";
|
|
12
13
|
import type * as public_ from "../public.js";
|
|
13
14
|
import type * as strava from "../strava.js";
|
|
@@ -33,6 +34,7 @@ import type {
|
|
|
33
34
|
import { anyApi, componentsGeneric } from "convex/server";
|
|
34
35
|
|
|
35
36
|
const fullApi: ApiFromModules<{
|
|
37
|
+
garmin: typeof garmin;
|
|
36
38
|
private: typeof private_;
|
|
37
39
|
public: typeof public_;
|
|
38
40
|
strava: typeof strava;
|
|
@@ -23,6 +23,68 @@ import type { FunctionReference } from "convex/server";
|
|
|
23
23
|
*/
|
|
24
24
|
export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
25
25
|
{
|
|
26
|
+
garmin: {
|
|
27
|
+
connectGarmin: FunctionReference<
|
|
28
|
+
"action",
|
|
29
|
+
"internal",
|
|
30
|
+
{
|
|
31
|
+
consumerKey: string;
|
|
32
|
+
consumerSecret: string;
|
|
33
|
+
token: string;
|
|
34
|
+
tokenSecret: string;
|
|
35
|
+
userId: string;
|
|
36
|
+
verifier: string;
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
connectionId: string;
|
|
40
|
+
errors: Array<{ error: string; id: string; type: string }>;
|
|
41
|
+
synced: {
|
|
42
|
+
activities: number;
|
|
43
|
+
body: number;
|
|
44
|
+
dailies: number;
|
|
45
|
+
menstruation: number;
|
|
46
|
+
sleep: number;
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
Name
|
|
50
|
+
>;
|
|
51
|
+
disconnectGarmin: FunctionReference<
|
|
52
|
+
"action",
|
|
53
|
+
"internal",
|
|
54
|
+
{ userId: string },
|
|
55
|
+
null,
|
|
56
|
+
Name
|
|
57
|
+
>;
|
|
58
|
+
getGarminRequestToken: FunctionReference<
|
|
59
|
+
"action",
|
|
60
|
+
"internal",
|
|
61
|
+
{ callbackUrl?: string; consumerKey: string; consumerSecret: string },
|
|
62
|
+
{ authUrl: string; token: string; tokenSecret: string },
|
|
63
|
+
Name
|
|
64
|
+
>;
|
|
65
|
+
syncGarmin: FunctionReference<
|
|
66
|
+
"action",
|
|
67
|
+
"internal",
|
|
68
|
+
{
|
|
69
|
+
consumerKey: string;
|
|
70
|
+
consumerSecret: string;
|
|
71
|
+
endTimeInSeconds?: number;
|
|
72
|
+
startTimeInSeconds?: number;
|
|
73
|
+
userId: string;
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
errors: Array<{ error: string; id: string; type: string }>;
|
|
77
|
+
synced: {
|
|
78
|
+
activities: number;
|
|
79
|
+
body: number;
|
|
80
|
+
dailies: number;
|
|
81
|
+
menstruation: number;
|
|
82
|
+
sleep: number;
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
Name
|
|
86
|
+
>;
|
|
87
|
+
};
|
|
26
88
|
public: {
|
|
27
89
|
connect: FunctionReference<
|
|
28
90
|
"mutation",
|