@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.
Files changed (101) hide show
  1. package/dist/client/index.d.ts +167 -0
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +150 -0
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +2 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +56 -0
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/garmin.d.ts +110 -0
  11. package/dist/component/garmin.d.ts.map +1 -0
  12. package/dist/component/garmin.js +454 -0
  13. package/dist/component/garmin.js.map +1 -0
  14. package/dist/component/public.d.ts +761 -761
  15. package/dist/component/schema.d.ts +390 -388
  16. package/dist/component/schema.d.ts.map +1 -1
  17. package/dist/component/schema.js +3 -2
  18. package/dist/component/schema.js.map +1 -1
  19. package/dist/component/strava.d.ts +5 -4
  20. package/dist/component/strava.d.ts.map +1 -1
  21. package/dist/component/strava.js +18 -1
  22. package/dist/component/strava.js.map +1 -1
  23. package/dist/component/validators/activity.d.ts +42 -42
  24. package/dist/component/validators/body.d.ts +47 -47
  25. package/dist/component/validators/daily.d.ts +17 -17
  26. package/dist/component/validators/plannedWorkout.d.ts +5 -5
  27. package/dist/component/validators/samples.d.ts +2 -2
  28. package/dist/component/validators/shared.d.ts +17 -17
  29. package/dist/component/validators/sleep.d.ts +17 -17
  30. package/dist/garmin/activity.d.ts +101 -0
  31. package/dist/garmin/activity.d.ts.map +1 -0
  32. package/dist/garmin/activity.js +207 -0
  33. package/dist/garmin/activity.js.map +1 -0
  34. package/dist/garmin/auth.d.ts +65 -0
  35. package/dist/garmin/auth.d.ts.map +1 -0
  36. package/dist/garmin/auth.js +155 -0
  37. package/dist/garmin/auth.js.map +1 -0
  38. package/dist/garmin/body.d.ts +26 -0
  39. package/dist/garmin/body.d.ts.map +1 -0
  40. package/dist/garmin/body.js +44 -0
  41. package/dist/garmin/body.js.map +1 -0
  42. package/dist/garmin/client.d.ts +99 -0
  43. package/dist/garmin/client.d.ts.map +1 -0
  44. package/dist/garmin/client.js +153 -0
  45. package/dist/garmin/client.js.map +1 -0
  46. package/dist/garmin/daily.d.ts +74 -0
  47. package/dist/garmin/daily.d.ts.map +1 -0
  48. package/dist/garmin/daily.js +143 -0
  49. package/dist/garmin/daily.js.map +1 -0
  50. package/dist/garmin/index.d.ts +20 -0
  51. package/dist/garmin/index.d.ts.map +1 -0
  52. package/dist/garmin/index.js +21 -0
  53. package/dist/garmin/index.js.map +1 -0
  54. package/dist/garmin/maps/activity-type.d.ts +7 -0
  55. package/dist/garmin/maps/activity-type.d.ts.map +1 -0
  56. package/dist/garmin/maps/activity-type.js +98 -0
  57. package/dist/garmin/maps/activity-type.js.map +1 -0
  58. package/dist/garmin/maps/sleep-level.d.ts +6 -0
  59. package/dist/garmin/maps/sleep-level.d.ts.map +1 -0
  60. package/dist/garmin/maps/sleep-level.js +21 -0
  61. package/dist/garmin/maps/sleep-level.js.map +1 -0
  62. package/dist/garmin/menstruation.d.ts +23 -0
  63. package/dist/garmin/menstruation.d.ts.map +1 -0
  64. package/dist/garmin/menstruation.js +34 -0
  65. package/dist/garmin/menstruation.js.map +1 -0
  66. package/dist/garmin/sleep.d.ts +62 -0
  67. package/dist/garmin/sleep.d.ts.map +1 -0
  68. package/dist/garmin/sleep.js +125 -0
  69. package/dist/garmin/sleep.js.map +1 -0
  70. package/dist/garmin/sync.d.ts +39 -0
  71. package/dist/garmin/sync.d.ts.map +1 -0
  72. package/dist/garmin/sync.js +175 -0
  73. package/dist/garmin/sync.js.map +1 -0
  74. package/dist/garmin/types.d.ts +212 -0
  75. package/dist/garmin/types.d.ts.map +1 -0
  76. package/dist/garmin/types.js +8 -0
  77. package/dist/garmin/types.js.map +1 -0
  78. package/dist/validators.d.ts +331 -331
  79. package/package.json +5 -1
  80. package/src/client/index.ts +194 -1
  81. package/src/component/_generated/api.ts +2 -0
  82. package/src/component/_generated/component.ts +62 -0
  83. package/src/component/garmin.ts +534 -0
  84. package/src/component/schema.ts +3 -2
  85. package/src/component/strava.ts +23 -1
  86. package/src/garmin/activity.test.ts +178 -0
  87. package/src/garmin/activity.ts +272 -0
  88. package/src/garmin/auth.test.ts +128 -0
  89. package/src/garmin/auth.ts +249 -0
  90. package/src/garmin/body.ts +59 -0
  91. package/src/garmin/client.ts +254 -0
  92. package/src/garmin/daily.ts +211 -0
  93. package/src/garmin/index.ts +76 -0
  94. package/src/garmin/maps/activity-type.test.ts +78 -0
  95. package/src/garmin/maps/activity-type.ts +116 -0
  96. package/src/garmin/maps/sleep-level.ts +22 -0
  97. package/src/garmin/menstruation.ts +42 -0
  98. package/src/garmin/sleep.test.ts +110 -0
  99. package/src/garmin/sleep.ts +170 -0
  100. package/src/garmin/sync.ts +223 -0
  101. 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.3.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"
@@ -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",