@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.
Files changed (104) hide show
  1. package/README.md +395 -0
  2. package/dist/client/index.d.ts +47 -6
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +30 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/types.d.ts +83 -0
  7. package/dist/client/types.d.ts.map +1 -1
  8. package/dist/client/types.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +50 -0
  10. package/dist/component/_generated/component.d.ts.map +1 -0
  11. package/dist/component/_generated/component.js +11 -0
  12. package/dist/component/_generated/component.js.map +1 -0
  13. package/dist/component/backfillJobs.d.ts +11 -11
  14. package/dist/component/connections.d.ts +9 -9
  15. package/dist/component/connections.d.ts.map +1 -1
  16. package/dist/component/connections.js +2 -0
  17. package/dist/component/connections.js.map +1 -1
  18. package/dist/component/dataPoints.d.ts +153 -39
  19. package/dist/component/dataPoints.d.ts.map +1 -1
  20. package/dist/component/dataPoints.js +1048 -139
  21. package/dist/component/dataPoints.js.map +1 -1
  22. package/dist/component/events.d.ts +13 -13
  23. package/dist/component/garminBackfill.d.ts +2 -2
  24. package/dist/component/garminWebhooks.d.ts +2 -2
  25. package/dist/component/garminWebhooks.d.ts.map +1 -1
  26. package/dist/component/garminWebhooks.js +2 -0
  27. package/dist/component/garminWebhooks.js.map +1 -1
  28. package/dist/component/lifecycle.d.ts +1 -1
  29. package/dist/component/lifecycle.d.ts.map +1 -1
  30. package/dist/component/lifecycle.js +39 -1
  31. package/dist/component/lifecycle.js.map +1 -1
  32. package/dist/component/oauthStates.d.ts +3 -3
  33. package/dist/component/schema.d.ts +192 -28
  34. package/dist/component/schema.d.ts.map +1 -1
  35. package/dist/component/schema.js +89 -0
  36. package/dist/component/schema.js.map +1 -1
  37. package/dist/component/sdkPush.d.ts +11 -11
  38. package/dist/component/summaries.d.ts +4 -4
  39. package/dist/component/syncJobs.d.ts +23 -23
  40. package/dist/component/syncWorkflow.d.ts +2 -2
  41. package/dist/component/timeSeriesPolicyUtils.d.ts +97 -0
  42. package/dist/component/timeSeriesPolicyUtils.d.ts.map +1 -0
  43. package/dist/component/timeSeriesPolicyUtils.js +163 -0
  44. package/dist/component/timeSeriesPolicyUtils.js.map +1 -0
  45. package/dist/test.d.ts +581 -0
  46. package/dist/test.d.ts.map +1 -0
  47. package/dist/test.js +17 -0
  48. package/dist/test.js.map +1 -0
  49. package/package.json +12 -2
  50. package/src/client/_generated/_ignore.ts +2 -0
  51. package/src/client/index.test.ts +149 -0
  52. package/src/client/index.ts +859 -0
  53. package/src/client/types.ts +632 -0
  54. package/src/component/_generated/_ignore.ts +2 -0
  55. package/src/component/_generated/api.ts +16 -0
  56. package/src/component/_generated/component.ts +74 -0
  57. package/src/component/_generated/dataModel.ts +40 -0
  58. package/src/component/_generated/server.ts +48 -0
  59. package/src/component/backfillJobs.test.ts +47 -0
  60. package/src/component/backfillJobs.ts +245 -0
  61. package/src/component/connections.test.ts +297 -0
  62. package/src/component/connections.ts +329 -0
  63. package/src/component/convex.config.ts +7 -0
  64. package/src/component/dataPoints.test.ts +827 -0
  65. package/src/component/dataPoints.ts +1676 -0
  66. package/src/component/dataSources.test.ts +247 -0
  67. package/src/component/dataSources.ts +109 -0
  68. package/src/component/events.test.ts +380 -0
  69. package/src/component/events.ts +288 -0
  70. package/src/component/garminBackfill.ts +343 -0
  71. package/src/component/garminWebhooks.test.ts +609 -0
  72. package/src/component/garminWebhooks.ts +656 -0
  73. package/src/component/httpHandlers.ts +153 -0
  74. package/src/component/lifecycle.test.ts +179 -0
  75. package/src/component/lifecycle.ts +128 -0
  76. package/src/component/menstrualCycles.ts +124 -0
  77. package/src/component/oauthActions.ts +261 -0
  78. package/src/component/oauthStates.test.ts +170 -0
  79. package/src/component/oauthStates.ts +85 -0
  80. package/src/component/providerSettings.ts +66 -0
  81. package/src/component/providers/additionalProviders.test.ts +401 -0
  82. package/src/component/providers/garmin.ts +1169 -0
  83. package/src/component/providers/oauth.test.ts +174 -0
  84. package/src/component/providers/oauth.ts +246 -0
  85. package/src/component/providers/polar.ts +220 -0
  86. package/src/component/providers/registry.ts +37 -0
  87. package/src/component/providers/strava.test.ts +195 -0
  88. package/src/component/providers/strava.ts +253 -0
  89. package/src/component/providers/suunto.ts +592 -0
  90. package/src/component/providers/types.ts +189 -0
  91. package/src/component/providers/whoop.ts +600 -0
  92. package/src/component/schema.ts +445 -0
  93. package/src/component/sdkPush.test.ts +367 -0
  94. package/src/component/sdkPush.ts +440 -0
  95. package/src/component/summaries.test.ts +201 -0
  96. package/src/component/summaries.ts +143 -0
  97. package/src/component/syncJobs.test.ts +254 -0
  98. package/src/component/syncJobs.ts +140 -0
  99. package/src/component/syncWorkflow.test.ts +87 -0
  100. package/src/component/syncWorkflow.ts +739 -0
  101. package/src/component/test.setup.ts +6 -0
  102. package/src/component/timeSeriesPolicyUtils.ts +243 -0
  103. package/src/component/workflowManager.ts +19 -0
  104. 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
+ });