@clipin/convex-wearables 0.0.2 → 0.1.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/README.md +395 -0
- package/dist/client/index.d.ts +47 -6
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +30 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +83 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/component/_generated/component.d.ts +50 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/backfillJobs.d.ts +11 -11
- package/dist/component/connections.d.ts +9 -9
- package/dist/component/connections.d.ts.map +1 -1
- package/dist/component/connections.js +2 -0
- package/dist/component/connections.js.map +1 -1
- package/dist/component/dataPoints.d.ts +153 -39
- package/dist/component/dataPoints.d.ts.map +1 -1
- package/dist/component/dataPoints.js +1048 -139
- package/dist/component/dataPoints.js.map +1 -1
- package/dist/component/events.d.ts +13 -13
- package/dist/component/garminBackfill.d.ts +2 -2
- package/dist/component/garminWebhooks.d.ts +2 -2
- package/dist/component/garminWebhooks.d.ts.map +1 -1
- package/dist/component/garminWebhooks.js +2 -0
- package/dist/component/garminWebhooks.js.map +1 -1
- package/dist/component/lifecycle.d.ts +1 -1
- package/dist/component/lifecycle.d.ts.map +1 -1
- package/dist/component/lifecycle.js +39 -1
- package/dist/component/lifecycle.js.map +1 -1
- package/dist/component/oauthStates.d.ts +3 -3
- package/dist/component/schema.d.ts +192 -28
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +89 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sdkPush.d.ts +11 -11
- package/dist/component/summaries.d.ts +4 -4
- package/dist/component/syncJobs.d.ts +23 -23
- package/dist/component/syncWorkflow.d.ts +2 -2
- package/dist/component/timeSeriesPolicyUtils.d.ts +97 -0
- package/dist/component/timeSeriesPolicyUtils.d.ts.map +1 -0
- package/dist/component/timeSeriesPolicyUtils.js +163 -0
- package/dist/component/timeSeriesPolicyUtils.js.map +1 -0
- package/dist/test.d.ts +581 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +17 -0
- package/dist/test.js.map +1 -0
- package/package.json +12 -2
- package/src/client/_generated/_ignore.ts +2 -0
- package/src/client/index.test.ts +149 -0
- package/src/client/index.ts +859 -0
- package/src/client/types.ts +632 -0
- package/src/component/_generated/_ignore.ts +2 -0
- package/src/component/_generated/api.ts +16 -0
- package/src/component/_generated/component.ts +74 -0
- package/src/component/_generated/dataModel.ts +40 -0
- package/src/component/_generated/server.ts +48 -0
- package/src/component/backfillJobs.test.ts +47 -0
- package/src/component/backfillJobs.ts +245 -0
- package/src/component/connections.test.ts +297 -0
- package/src/component/connections.ts +329 -0
- package/src/component/convex.config.ts +7 -0
- package/src/component/dataPoints.test.ts +827 -0
- package/src/component/dataPoints.ts +1676 -0
- package/src/component/dataSources.test.ts +247 -0
- package/src/component/dataSources.ts +109 -0
- package/src/component/events.test.ts +380 -0
- package/src/component/events.ts +288 -0
- package/src/component/garminBackfill.ts +343 -0
- package/src/component/garminWebhooks.test.ts +609 -0
- package/src/component/garminWebhooks.ts +656 -0
- package/src/component/httpHandlers.ts +153 -0
- package/src/component/lifecycle.test.ts +179 -0
- package/src/component/lifecycle.ts +128 -0
- package/src/component/menstrualCycles.ts +124 -0
- package/src/component/oauthActions.ts +261 -0
- package/src/component/oauthStates.test.ts +170 -0
- package/src/component/oauthStates.ts +85 -0
- package/src/component/providerSettings.ts +66 -0
- package/src/component/providers/additionalProviders.test.ts +401 -0
- package/src/component/providers/garmin.ts +1169 -0
- package/src/component/providers/oauth.test.ts +174 -0
- package/src/component/providers/oauth.ts +246 -0
- package/src/component/providers/polar.ts +220 -0
- package/src/component/providers/registry.ts +37 -0
- package/src/component/providers/strava.test.ts +195 -0
- package/src/component/providers/strava.ts +253 -0
- package/src/component/providers/suunto.ts +592 -0
- package/src/component/providers/types.ts +189 -0
- package/src/component/providers/whoop.ts +600 -0
- package/src/component/schema.ts +445 -0
- package/src/component/sdkPush.test.ts +367 -0
- package/src/component/sdkPush.ts +440 -0
- package/src/component/summaries.test.ts +201 -0
- package/src/component/summaries.ts +143 -0
- package/src/component/syncJobs.test.ts +254 -0
- package/src/component/syncJobs.ts +140 -0
- package/src/component/syncWorkflow.test.ts +87 -0
- package/src/component/syncWorkflow.ts +739 -0
- package/src/component/test.setup.ts +6 -0
- package/src/component/timeSeriesPolicyUtils.ts +243 -0
- package/src/component/workflowManager.ts +19 -0
- package/src/test.ts +25 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { convexTest } from "convex-test";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import schema from "./schema";
|
|
4
|
+
import { modules } from "./test.setup";
|
|
5
|
+
|
|
6
|
+
describe("dataSources", () => {
|
|
7
|
+
it("creates a new data source", async () => {
|
|
8
|
+
const t = convexTest(schema, modules);
|
|
9
|
+
|
|
10
|
+
const id = await t.run(async (ctx) => {
|
|
11
|
+
return await ctx.db.insert("dataSources", {
|
|
12
|
+
userId: "user-1",
|
|
13
|
+
provider: "strava",
|
|
14
|
+
source: "strava",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const ds = await t.run(async (ctx) => {
|
|
19
|
+
return await ctx.db.get(id);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(ds?.userId).toBe("user-1");
|
|
23
|
+
expect(ds?.provider).toBe("strava");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("finds data sources by user via index", async () => {
|
|
27
|
+
const t = convexTest(schema, modules);
|
|
28
|
+
|
|
29
|
+
await t.run(async (ctx) => {
|
|
30
|
+
await ctx.db.insert("dataSources", {
|
|
31
|
+
userId: "user-1",
|
|
32
|
+
provider: "strava",
|
|
33
|
+
source: "strava",
|
|
34
|
+
});
|
|
35
|
+
await ctx.db.insert("dataSources", {
|
|
36
|
+
userId: "user-1",
|
|
37
|
+
provider: "garmin",
|
|
38
|
+
deviceModel: "Forerunner 965",
|
|
39
|
+
source: "garmin-connect",
|
|
40
|
+
});
|
|
41
|
+
await ctx.db.insert("dataSources", {
|
|
42
|
+
userId: "user-2",
|
|
43
|
+
provider: "whoop",
|
|
44
|
+
source: "whoop",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const user1Sources = await t.run(async (ctx) => {
|
|
49
|
+
return await ctx.db
|
|
50
|
+
.query("dataSources")
|
|
51
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-1"))
|
|
52
|
+
.collect();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(user1Sources).toHaveLength(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("finds data sources by user + provider via index", async () => {
|
|
59
|
+
const t = convexTest(schema, modules);
|
|
60
|
+
|
|
61
|
+
await t.run(async (ctx) => {
|
|
62
|
+
await ctx.db.insert("dataSources", {
|
|
63
|
+
userId: "user-1",
|
|
64
|
+
provider: "strava",
|
|
65
|
+
source: "strava",
|
|
66
|
+
});
|
|
67
|
+
await ctx.db.insert("dataSources", {
|
|
68
|
+
userId: "user-1",
|
|
69
|
+
provider: "garmin",
|
|
70
|
+
source: "garmin-connect",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const stravaSources = await t.run(async (ctx) => {
|
|
75
|
+
return await ctx.db
|
|
76
|
+
.query("dataSources")
|
|
77
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-1").eq("provider", "strava"))
|
|
78
|
+
.collect();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(stravaSources).toHaveLength(1);
|
|
82
|
+
expect(stravaSources[0].provider).toBe("strava");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("upserts by user/provider/device/source (getOrCreate pattern)", async () => {
|
|
86
|
+
const t = convexTest(schema, modules);
|
|
87
|
+
|
|
88
|
+
// Create initial data source
|
|
89
|
+
const id1 = await t.run(async (ctx) => {
|
|
90
|
+
return await ctx.db.insert("dataSources", {
|
|
91
|
+
userId: "user-1",
|
|
92
|
+
provider: "garmin",
|
|
93
|
+
deviceModel: "Forerunner 965",
|
|
94
|
+
source: "garmin-connect",
|
|
95
|
+
softwareVersion: "1.0",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Simulate getOrCreate — same user/provider/device/source should find existing
|
|
100
|
+
const found = await t.run(async (ctx) => {
|
|
101
|
+
return await ctx.db
|
|
102
|
+
.query("dataSources")
|
|
103
|
+
.withIndex("by_user_provider_device", (idx) =>
|
|
104
|
+
idx
|
|
105
|
+
.eq("userId", "user-1")
|
|
106
|
+
.eq("provider", "garmin")
|
|
107
|
+
.eq("deviceModel", "Forerunner 965")
|
|
108
|
+
.eq("source", "garmin-connect"),
|
|
109
|
+
)
|
|
110
|
+
.first();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(found?._id).toBe(id1);
|
|
114
|
+
|
|
115
|
+
// Update the software version (simulating upsert update)
|
|
116
|
+
await t.run(async (ctx) => {
|
|
117
|
+
if (found) {
|
|
118
|
+
await ctx.db.patch(found._id, { softwareVersion: "2.0" });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const updated = await t.run(async (ctx) => {
|
|
123
|
+
return await ctx.db.get(id1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(updated?.softwareVersion).toBe("2.0");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("creates separate entries for different devices", async () => {
|
|
130
|
+
const t = convexTest(schema, modules);
|
|
131
|
+
|
|
132
|
+
await t.run(async (ctx) => {
|
|
133
|
+
await ctx.db.insert("dataSources", {
|
|
134
|
+
userId: "user-1",
|
|
135
|
+
provider: "garmin",
|
|
136
|
+
deviceModel: "Forerunner 965",
|
|
137
|
+
source: "garmin-connect",
|
|
138
|
+
});
|
|
139
|
+
await ctx.db.insert("dataSources", {
|
|
140
|
+
userId: "user-1",
|
|
141
|
+
provider: "garmin",
|
|
142
|
+
deviceModel: "Venu 3",
|
|
143
|
+
source: "garmin-connect",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const sources = await t.run(async (ctx) => {
|
|
148
|
+
return await ctx.db
|
|
149
|
+
.query("dataSources")
|
|
150
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-1").eq("provider", "garmin"))
|
|
151
|
+
.collect();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(sources).toHaveLength(2);
|
|
155
|
+
const devices = sources.map((s) => s.deviceModel);
|
|
156
|
+
expect(devices).toContain("Forerunner 965");
|
|
157
|
+
expect(devices).toContain("Venu 3");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("finds data sources by connectionId", async () => {
|
|
161
|
+
const t = convexTest(schema, modules);
|
|
162
|
+
|
|
163
|
+
const connId = await t.run(async (ctx) => {
|
|
164
|
+
return await ctx.db.insert("connections", {
|
|
165
|
+
userId: "user-1",
|
|
166
|
+
provider: "strava",
|
|
167
|
+
accessToken: "tok",
|
|
168
|
+
status: "active",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await t.run(async (ctx) => {
|
|
173
|
+
await ctx.db.insert("dataSources", {
|
|
174
|
+
userId: "user-1",
|
|
175
|
+
provider: "strava",
|
|
176
|
+
connectionId: connId,
|
|
177
|
+
source: "strava",
|
|
178
|
+
});
|
|
179
|
+
// Unrelated data source
|
|
180
|
+
await ctx.db.insert("dataSources", {
|
|
181
|
+
userId: "user-2",
|
|
182
|
+
provider: "garmin",
|
|
183
|
+
source: "garmin",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const byConn = await t.run(async (ctx) => {
|
|
188
|
+
return await ctx.db
|
|
189
|
+
.query("dataSources")
|
|
190
|
+
.withIndex("by_connection", (idx) => idx.eq("connectionId", connId))
|
|
191
|
+
.collect();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(byConn).toHaveLength(1);
|
|
195
|
+
expect(byConn[0].provider).toBe("strava");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("deletes all data sources for a user", async () => {
|
|
199
|
+
const t = convexTest(schema, modules);
|
|
200
|
+
|
|
201
|
+
await t.run(async (ctx) => {
|
|
202
|
+
await ctx.db.insert("dataSources", {
|
|
203
|
+
userId: "user-1",
|
|
204
|
+
provider: "strava",
|
|
205
|
+
source: "strava",
|
|
206
|
+
});
|
|
207
|
+
await ctx.db.insert("dataSources", {
|
|
208
|
+
userId: "user-1",
|
|
209
|
+
provider: "garmin",
|
|
210
|
+
source: "garmin",
|
|
211
|
+
});
|
|
212
|
+
await ctx.db.insert("dataSources", {
|
|
213
|
+
userId: "user-2",
|
|
214
|
+
provider: "whoop",
|
|
215
|
+
source: "whoop",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Delete user-1 sources
|
|
220
|
+
await t.run(async (ctx) => {
|
|
221
|
+
const sources = await ctx.db
|
|
222
|
+
.query("dataSources")
|
|
223
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-1"))
|
|
224
|
+
.collect();
|
|
225
|
+
for (const s of sources) {
|
|
226
|
+
await ctx.db.delete(s._id);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const user1 = await t.run(async (ctx) => {
|
|
231
|
+
return await ctx.db
|
|
232
|
+
.query("dataSources")
|
|
233
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-1"))
|
|
234
|
+
.collect();
|
|
235
|
+
});
|
|
236
|
+
expect(user1).toHaveLength(0);
|
|
237
|
+
|
|
238
|
+
// user-2 unaffected
|
|
239
|
+
const user2 = await t.run(async (ctx) => {
|
|
240
|
+
return await ctx.db
|
|
241
|
+
.query("dataSources")
|
|
242
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", "user-2"))
|
|
243
|
+
.collect();
|
|
244
|
+
});
|
|
245
|
+
expect(user2).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internalMutation, mutation, query } from "./_generated/server";
|
|
3
|
+
import { providerName } from "./schema";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Queries
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get all data sources for a user.
|
|
11
|
+
*/
|
|
12
|
+
export const getByUser = query({
|
|
13
|
+
args: { userId: v.string() },
|
|
14
|
+
returns: v.array(v.any()),
|
|
15
|
+
handler: async (ctx, args) => {
|
|
16
|
+
return await ctx.db
|
|
17
|
+
.query("dataSources")
|
|
18
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", args.userId))
|
|
19
|
+
.collect();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get data sources for a user + provider combination.
|
|
25
|
+
*/
|
|
26
|
+
export const getByUserProvider = query({
|
|
27
|
+
args: {
|
|
28
|
+
userId: v.string(),
|
|
29
|
+
provider: providerName,
|
|
30
|
+
},
|
|
31
|
+
returns: v.array(v.any()),
|
|
32
|
+
handler: async (ctx, args) => {
|
|
33
|
+
return await ctx.db
|
|
34
|
+
.query("dataSources")
|
|
35
|
+
.withIndex("by_user_provider", (idx) =>
|
|
36
|
+
idx.eq("userId", args.userId).eq("provider", args.provider),
|
|
37
|
+
)
|
|
38
|
+
.collect();
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Mutations
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get or create a data source. Used during data ingestion to ensure
|
|
48
|
+
* a data source exists for a given user/provider/device combination.
|
|
49
|
+
*/
|
|
50
|
+
export const getOrCreate = mutation({
|
|
51
|
+
args: {
|
|
52
|
+
userId: v.string(),
|
|
53
|
+
provider: providerName,
|
|
54
|
+
connectionId: v.optional(v.id("connections")),
|
|
55
|
+
deviceModel: v.optional(v.string()),
|
|
56
|
+
softwareVersion: v.optional(v.string()),
|
|
57
|
+
source: v.optional(v.string()),
|
|
58
|
+
deviceType: v.optional(v.string()),
|
|
59
|
+
originalSourceName: v.optional(v.string()),
|
|
60
|
+
},
|
|
61
|
+
returns: v.id("dataSources"),
|
|
62
|
+
handler: async (ctx, args) => {
|
|
63
|
+
// Look for existing data source matching this user/provider/device/source
|
|
64
|
+
const existing = await ctx.db
|
|
65
|
+
.query("dataSources")
|
|
66
|
+
.withIndex("by_user_provider_device", (idx) =>
|
|
67
|
+
idx
|
|
68
|
+
.eq("userId", args.userId)
|
|
69
|
+
.eq("provider", args.provider)
|
|
70
|
+
.eq("deviceModel", args.deviceModel ?? undefined)
|
|
71
|
+
.eq("source", args.source ?? undefined),
|
|
72
|
+
)
|
|
73
|
+
.first();
|
|
74
|
+
|
|
75
|
+
if (existing) {
|
|
76
|
+
// Update fields that may have changed
|
|
77
|
+
if (
|
|
78
|
+
args.softwareVersion !== existing.softwareVersion ||
|
|
79
|
+
args.deviceType !== existing.deviceType
|
|
80
|
+
) {
|
|
81
|
+
await ctx.db.patch(existing._id, {
|
|
82
|
+
softwareVersion: args.softwareVersion,
|
|
83
|
+
deviceType: args.deviceType,
|
|
84
|
+
connectionId: args.connectionId ?? existing.connectionId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return existing._id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return await ctx.db.insert("dataSources", args);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Delete all data sources for a user. Used during account deletion.
|
|
96
|
+
*/
|
|
97
|
+
export const deleteByUser = internalMutation({
|
|
98
|
+
args: { userId: v.string() },
|
|
99
|
+
handler: async (ctx, args) => {
|
|
100
|
+
const sources = await ctx.db
|
|
101
|
+
.query("dataSources")
|
|
102
|
+
.withIndex("by_user_provider", (idx) => idx.eq("userId", args.userId))
|
|
103
|
+
.collect();
|
|
104
|
+
|
|
105
|
+
for (const source of sources) {
|
|
106
|
+
await ctx.db.delete(source._id);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|