@nativesquare/soma 0.5.0 → 0.7.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 +151 -53
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +162 -69
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +130 -17
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/garmin.d.ts +61 -43
- package/dist/component/garmin.d.ts.map +1 -1
- package/dist/component/garmin.js +208 -122
- package/dist/component/garmin.js.map +1 -1
- package/dist/component/public.d.ts +363 -0
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +124 -0
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +7 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +9 -10
- package/dist/component/schema.js.map +1 -1
- package/dist/component/strava.d.ts +0 -1
- package/dist/component/strava.d.ts.map +1 -1
- package/dist/component/strava.js +0 -1
- package/dist/component/strava.js.map +1 -1
- package/dist/component/validators/enums.d.ts +1 -1
- package/dist/garmin/auth.d.ts +55 -46
- package/dist/garmin/auth.d.ts.map +1 -1
- package/dist/garmin/auth.js +82 -122
- package/dist/garmin/auth.js.map +1 -1
- package/dist/garmin/client.d.ts +64 -17
- package/dist/garmin/client.d.ts.map +1 -1
- package/dist/garmin/client.js +143 -29
- package/dist/garmin/client.js.map +1 -1
- package/dist/garmin/index.d.ts +3 -3
- package/dist/garmin/index.d.ts.map +1 -1
- package/dist/garmin/index.js +4 -4
- package/dist/garmin/index.js.map +1 -1
- package/dist/garmin/plannedWorkout.d.ts +12 -0
- package/dist/garmin/plannedWorkout.d.ts.map +1 -0
- package/dist/garmin/plannedWorkout.js +267 -0
- package/dist/garmin/plannedWorkout.js.map +1 -0
- package/dist/garmin/types.d.ts +78 -6
- package/dist/garmin/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +236 -85
- package/src/component/_generated/component.ts +155 -17
- package/src/component/garmin.ts +258 -124
- package/src/component/public.ts +135 -0
- package/src/component/schema.ts +9 -10
- package/src/component/strava.ts +0 -1
- package/src/garmin/auth.test.ts +71 -96
- package/src/garmin/auth.ts +129 -193
- package/src/garmin/client.ts +197 -51
- package/src/garmin/index.ts +13 -14
- package/src/garmin/plannedWorkout.ts +333 -0
- package/src/garmin/types.ts +149 -7
package/src/component/public.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { dailyValidator } from "./validators/daily.js";
|
|
|
8
8
|
import { sleepValidator } from "./validators/sleep.js";
|
|
9
9
|
import { menstruationValidator } from "./validators/menstruation.js";
|
|
10
10
|
import { nutritionValidator } from "./validators/nutrition.js";
|
|
11
|
+
import { plannedWorkoutValidator } from "./validators/plannedWorkout.js";
|
|
11
12
|
|
|
12
13
|
// ─── Return Validators ──────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -776,6 +777,140 @@ export const paginateMenstruation = query({
|
|
|
776
777
|
},
|
|
777
778
|
});
|
|
778
779
|
|
|
780
|
+
// ── Planned Workouts ────────────────────────────────────────────────────────
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Ingest a planned workout record.
|
|
784
|
+
*
|
|
785
|
+
* Upserts by `connectionId + metadata.id` when an id is present.
|
|
786
|
+
* Falls back to insert if no id is provided.
|
|
787
|
+
*/
|
|
788
|
+
export const ingestPlannedWorkout = mutation({
|
|
789
|
+
args: plannedWorkoutValidator,
|
|
790
|
+
returns: v.id("plannedWorkouts"),
|
|
791
|
+
handler: async (ctx, args) => {
|
|
792
|
+
const metadataId = args.metadata.id;
|
|
793
|
+
if (metadataId) {
|
|
794
|
+
const results = await ctx.db
|
|
795
|
+
.query("plannedWorkouts")
|
|
796
|
+
.withIndex("by_connectionId", (q) =>
|
|
797
|
+
q.eq("connectionId", args.connectionId),
|
|
798
|
+
)
|
|
799
|
+
.collect();
|
|
800
|
+
const existing = results.find((r) => r.metadata.id === metadataId);
|
|
801
|
+
|
|
802
|
+
if (existing) {
|
|
803
|
+
await ctx.db.patch(existing._id, args);
|
|
804
|
+
return existing._id;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return await ctx.db.insert("plannedWorkouts", args);
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* List planned workout records for a user, optionally filtered by planned date range.
|
|
813
|
+
*
|
|
814
|
+
* @param args.userId - The host app's user identifier
|
|
815
|
+
* @param args.startDate - Optional lower bound (inclusive) on metadata.planned_date (YYYY-MM-DD)
|
|
816
|
+
* @param args.endDate - Optional upper bound (inclusive) on metadata.planned_date (YYYY-MM-DD)
|
|
817
|
+
* @param args.order - Sort order: "asc" or "desc" (default: "desc")
|
|
818
|
+
* @param args.limit - Optional max number of results to return
|
|
819
|
+
*/
|
|
820
|
+
export const listPlannedWorkouts = query({
|
|
821
|
+
args: {
|
|
822
|
+
userId: v.string(),
|
|
823
|
+
startDate: v.optional(v.string()),
|
|
824
|
+
endDate: v.optional(v.string()),
|
|
825
|
+
order: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
|
|
826
|
+
limit: v.optional(v.number()),
|
|
827
|
+
},
|
|
828
|
+
handler: async (ctx, args) => {
|
|
829
|
+
const q = ctx.db
|
|
830
|
+
.query("plannedWorkouts")
|
|
831
|
+
.withIndex("by_userId_plannedDate", (q) => {
|
|
832
|
+
const base = q.eq("userId", args.userId);
|
|
833
|
+
if (args.startDate !== undefined && args.endDate !== undefined) {
|
|
834
|
+
return base
|
|
835
|
+
.gte("metadata.planned_date", args.startDate)
|
|
836
|
+
.lte("metadata.planned_date", args.endDate);
|
|
837
|
+
}
|
|
838
|
+
if (args.startDate !== undefined) {
|
|
839
|
+
return base.gte("metadata.planned_date", args.startDate);
|
|
840
|
+
}
|
|
841
|
+
if (args.endDate !== undefined) {
|
|
842
|
+
return base.lte("metadata.planned_date", args.endDate);
|
|
843
|
+
}
|
|
844
|
+
return base;
|
|
845
|
+
})
|
|
846
|
+
.order(args.order ?? "desc");
|
|
847
|
+
return args.limit ? await q.take(args.limit) : await q.collect();
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Paginate planned workout records for a user, optionally filtered by planned date range.
|
|
853
|
+
*
|
|
854
|
+
* Returns `{ page, isDone, continueCursor }` for cursor-based pagination.
|
|
855
|
+
*/
|
|
856
|
+
export const paginatePlannedWorkouts = query({
|
|
857
|
+
args: {
|
|
858
|
+
userId: v.string(),
|
|
859
|
+
startDate: v.optional(v.string()),
|
|
860
|
+
endDate: v.optional(v.string()),
|
|
861
|
+
paginationOpts: paginationOptsValidator,
|
|
862
|
+
},
|
|
863
|
+
handler: async (ctx, args) => {
|
|
864
|
+
return await ctx.db
|
|
865
|
+
.query("plannedWorkouts")
|
|
866
|
+
.withIndex("by_userId_plannedDate", (q) => {
|
|
867
|
+
const base = q.eq("userId", args.userId);
|
|
868
|
+
if (args.startDate !== undefined && args.endDate !== undefined) {
|
|
869
|
+
return base
|
|
870
|
+
.gte("metadata.planned_date", args.startDate)
|
|
871
|
+
.lte("metadata.planned_date", args.endDate);
|
|
872
|
+
}
|
|
873
|
+
if (args.startDate !== undefined) {
|
|
874
|
+
return base.gte("metadata.planned_date", args.startDate);
|
|
875
|
+
}
|
|
876
|
+
if (args.endDate !== undefined) {
|
|
877
|
+
return base.lte("metadata.planned_date", args.endDate);
|
|
878
|
+
}
|
|
879
|
+
return base;
|
|
880
|
+
})
|
|
881
|
+
.order("desc")
|
|
882
|
+
.paginate(args.paginationOpts);
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Delete a planned workout by document ID.
|
|
888
|
+
*/
|
|
889
|
+
export const deletePlannedWorkout = mutation({
|
|
890
|
+
args: { plannedWorkoutId: v.id("plannedWorkouts") },
|
|
891
|
+
returns: v.null(),
|
|
892
|
+
handler: async (ctx, args) => {
|
|
893
|
+
const existing = await ctx.db.get(args.plannedWorkoutId);
|
|
894
|
+
if (!existing) {
|
|
895
|
+
throw new Error(
|
|
896
|
+
`Planned workout "${args.plannedWorkoutId}" not found`,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
await ctx.db.delete(existing._id);
|
|
900
|
+
return null;
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Get a single planned workout by document ID.
|
|
906
|
+
*/
|
|
907
|
+
export const getPlannedWorkout = query({
|
|
908
|
+
args: { plannedWorkoutId: v.id("plannedWorkouts") },
|
|
909
|
+
handler: async (ctx, args) => {
|
|
910
|
+
return await ctx.db.get(args.plannedWorkoutId);
|
|
911
|
+
},
|
|
912
|
+
});
|
|
913
|
+
|
|
779
914
|
// ── Athletes ────────────────────────────────────────────────────────────────
|
|
780
915
|
|
|
781
916
|
/**
|
package/src/component/schema.ts
CHANGED
|
@@ -115,26 +115,25 @@ export default defineSchema({
|
|
|
115
115
|
.index("by_userId_plannedDate", ["userId", "metadata.planned_date"]),
|
|
116
116
|
|
|
117
117
|
// ── Provider Tokens ────────────────────────────────────────────────────────
|
|
118
|
-
// OAuth tokens for cloud-based providers (Strava, Garmin, etc.).
|
|
118
|
+
// OAuth 2.0 tokens for cloud-based providers (Strava, Garmin, etc.).
|
|
119
119
|
// Stored separately from connections to keep the connection table
|
|
120
120
|
// provider-agnostic. One token record per connection.
|
|
121
121
|
providerTokens: defineTable({
|
|
122
122
|
connectionId: v.id("connections"),
|
|
123
123
|
accessToken: v.string(),
|
|
124
|
-
refreshToken: v.optional(v.string()),
|
|
125
|
-
|
|
126
|
-
expiresAt: v.optional(v.number()), // Unix epoch seconds; absent for permanent tokens
|
|
124
|
+
refreshToken: v.optional(v.string()),
|
|
125
|
+
expiresAt: v.optional(v.number()),
|
|
127
126
|
}).index("by_connectionId", ["connectionId"]),
|
|
128
127
|
|
|
129
128
|
// ── Pending OAuth ─────────────────────────────────────────────────────────
|
|
130
|
-
// Temporary storage for in-progress OAuth flows. Bridges the gap
|
|
131
|
-
// initiating OAuth (
|
|
132
|
-
//
|
|
129
|
+
// Temporary storage for in-progress OAuth 2.0 PKCE flows. Bridges the gap
|
|
130
|
+
// between initiating OAuth (auth URL) and the callback (code exchange).
|
|
131
|
+
// The `state` parameter links the callback back to the pending entry.
|
|
133
132
|
pendingOAuth: defineTable({
|
|
134
133
|
provider: v.string(),
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
state: v.string(),
|
|
135
|
+
codeVerifier: v.string(),
|
|
137
136
|
userId: v.string(),
|
|
138
137
|
createdAt: v.number(),
|
|
139
|
-
}).index("
|
|
138
|
+
}).index("by_state", ["state"]),
|
|
140
139
|
});
|
package/src/component/strava.ts
CHANGED
package/src/garmin/auth.test.ts
CHANGED
|
@@ -1,128 +1,103 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
getTimestamp,
|
|
3
|
+
generateCodeVerifier,
|
|
4
|
+
generateCodeChallenge,
|
|
5
|
+
generateState,
|
|
6
|
+
buildAuthUrl,
|
|
8
7
|
} from "./auth.js";
|
|
9
8
|
|
|
10
|
-
describe("
|
|
11
|
-
it("
|
|
12
|
-
|
|
13
|
-
expect(
|
|
14
|
-
expect(percentEncode("test!")).toBe("test%21");
|
|
15
|
-
expect(percentEncode("100%")).toBe("100%25");
|
|
9
|
+
describe("generateCodeVerifier", () => {
|
|
10
|
+
it("returns a 64-character string by default", () => {
|
|
11
|
+
const verifier = generateCodeVerifier();
|
|
12
|
+
expect(verifier).toHaveLength(64);
|
|
16
13
|
});
|
|
17
14
|
|
|
18
|
-
it("
|
|
19
|
-
|
|
20
|
-
expect(
|
|
21
|
-
expect(percentEncode("a*b")).toBe("a%2Ab");
|
|
15
|
+
it("only contains valid PKCE characters", () => {
|
|
16
|
+
const verifier = generateCodeVerifier();
|
|
17
|
+
expect(verifier).toMatch(/^[A-Za-z0-9\-._~]+$/);
|
|
22
18
|
});
|
|
23
19
|
|
|
24
|
-
it("
|
|
25
|
-
|
|
26
|
-
expect(
|
|
20
|
+
it("respects custom length", () => {
|
|
21
|
+
const verifier = generateCodeVerifier(128);
|
|
22
|
+
expect(verifier).toHaveLength(128);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("generates unique values", () => {
|
|
26
|
+
const v1 = generateCodeVerifier();
|
|
27
|
+
const v2 = generateCodeVerifier();
|
|
28
|
+
expect(v1).not.toBe(v2);
|
|
27
29
|
});
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
describe("
|
|
31
|
-
it("returns a
|
|
32
|
-
const
|
|
33
|
-
expect(
|
|
34
|
-
expect(
|
|
32
|
+
describe("generateState", () => {
|
|
33
|
+
it("returns a 64-character hex string", () => {
|
|
34
|
+
const state = generateState();
|
|
35
|
+
expect(state).toHaveLength(64);
|
|
36
|
+
expect(state).toMatch(/^[0-9a-f]+$/);
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
it("generates unique values", () => {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
expect(
|
|
40
|
+
const s1 = generateState();
|
|
41
|
+
const s2 = generateState();
|
|
42
|
+
expect(s1).not.toBe(s2);
|
|
41
43
|
});
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
describe("
|
|
45
|
-
it("
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
expect(
|
|
49
|
-
expect(
|
|
46
|
+
describe("generateCodeChallenge", () => {
|
|
47
|
+
it("produces a base64url-encoded string", async () => {
|
|
48
|
+
const verifier = generateCodeVerifier();
|
|
49
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
50
|
+
expect(challenge).toMatch(/^[A-Za-z0-9\-_]+$/);
|
|
51
|
+
expect(challenge).not.toContain("+");
|
|
52
|
+
expect(challenge).not.toContain("/");
|
|
53
|
+
expect(challenge).not.toContain("=");
|
|
50
54
|
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("buildOAuthSignature", () => {
|
|
54
|
-
it("produces a valid HMAC-SHA1 signature", async () => {
|
|
55
|
-
const signature = await buildOAuthSignature(
|
|
56
|
-
"GET",
|
|
57
|
-
"https://api.example.com/resource",
|
|
58
|
-
{
|
|
59
|
-
oauth_consumer_key: "consumer_key",
|
|
60
|
-
oauth_nonce: "kllo9940pd9333jh",
|
|
61
|
-
oauth_signature_method: "HMAC-SHA1",
|
|
62
|
-
oauth_timestamp: "1191242096",
|
|
63
|
-
oauth_version: "1.0",
|
|
64
|
-
},
|
|
65
|
-
"consumer_secret",
|
|
66
|
-
"",
|
|
67
|
-
);
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
it("produces different challenges for different verifiers", async () => {
|
|
57
|
+
const c1 = await generateCodeChallenge("verifier_one_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
58
|
+
const c2 = await generateCodeChallenge("verifier_two_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
59
|
+
expect(c1).not.toBe(c2);
|
|
72
60
|
});
|
|
73
61
|
|
|
74
|
-
it("
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
62
|
+
it("produces consistent challenges for the same verifier", async () => {
|
|
63
|
+
const verifier = "consistent_verifier_test_aaaaaaaaaaaaaaaaaa";
|
|
64
|
+
const c1 = await generateCodeChallenge(verifier);
|
|
65
|
+
const c2 = await generateCodeChallenge(verifier);
|
|
66
|
+
expect(c1).toBe(c2);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
);
|
|
70
|
+
describe("buildAuthUrl", () => {
|
|
71
|
+
it("builds a valid authorization URL with required params", () => {
|
|
72
|
+
const url = buildAuthUrl({
|
|
73
|
+
clientId: "test-client-id",
|
|
74
|
+
codeChallenge: "test-challenge",
|
|
75
|
+
});
|
|
90
76
|
|
|
91
|
-
expect(
|
|
77
|
+
expect(url).toContain("https://connect.garmin.com/oauth2Confirm?");
|
|
78
|
+
expect(url).toContain("client_id=test-client-id");
|
|
79
|
+
expect(url).toContain("response_type=code");
|
|
80
|
+
expect(url).toContain("code_challenge=test-challenge");
|
|
81
|
+
expect(url).toContain("code_challenge_method=S256");
|
|
92
82
|
});
|
|
93
83
|
|
|
94
|
-
it("
|
|
95
|
-
const
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
const sig2 = await buildOAuthSignature(
|
|
102
|
-
"GET",
|
|
103
|
-
"https://api.example.com/resource",
|
|
104
|
-
{ a_param: "first", m_param: "middle", z_param: "last" },
|
|
105
|
-
"secret",
|
|
106
|
-
);
|
|
84
|
+
it("includes optional redirect_uri", () => {
|
|
85
|
+
const url = buildAuthUrl({
|
|
86
|
+
clientId: "test-client-id",
|
|
87
|
+
codeChallenge: "test-challenge",
|
|
88
|
+
redirectUri: "https://example.com/callback",
|
|
89
|
+
});
|
|
107
90
|
|
|
108
|
-
expect(
|
|
91
|
+
expect(url).toContain("redirect_uri=https%3A%2F%2Fexample.com%2Fcallback");
|
|
109
92
|
});
|
|
110
|
-
});
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
oauth_signature: "sig%3D",
|
|
118
|
-
oauth_signature_method: "HMAC-SHA1",
|
|
119
|
-
oauth_timestamp: "1234567890",
|
|
120
|
-
oauth_version: "1.0",
|
|
94
|
+
it("includes optional state", () => {
|
|
95
|
+
const url = buildAuthUrl({
|
|
96
|
+
clientId: "test-client-id",
|
|
97
|
+
codeChallenge: "test-challenge",
|
|
98
|
+
state: "my-state-value",
|
|
121
99
|
});
|
|
122
100
|
|
|
123
|
-
expect(
|
|
124
|
-
expect(header).toContain('oauth_consumer_key="my_key"');
|
|
125
|
-
expect(header).toContain('oauth_nonce="abc123"');
|
|
126
|
-
expect(header).toContain('oauth_version="1.0"');
|
|
101
|
+
expect(url).toContain("state=my-state-value");
|
|
127
102
|
});
|
|
128
103
|
});
|