@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,74 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `ComponentApi` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
12
+ import type { GenericId as ConvexId } from "convex/values";
13
+
14
+ type Modules = {
15
+ backfillJobs: typeof import("../backfillJobs.js");
16
+ connections: typeof import("../connections.js");
17
+ dataPoints: typeof import("../dataPoints.js");
18
+ dataSources: typeof import("../dataSources.js");
19
+ events: typeof import("../events.js");
20
+ garminBackfill: typeof import("../garminBackfill.js");
21
+ garminWebhooks: typeof import("../garminWebhooks.js");
22
+ lifecycle: typeof import("../lifecycle.js");
23
+ menstrualCycles: typeof import("../menstrualCycles.js");
24
+ oauthActions: typeof import("../oauthActions.js");
25
+ sdkPush: typeof import("../sdkPush.js");
26
+ summaries: typeof import("../summaries.js");
27
+ syncJobs: typeof import("../syncJobs.js");
28
+ syncWorkflow: typeof import("../syncWorkflow.js");
29
+ };
30
+
31
+ type PublicApi = FilterApi<
32
+ ApiFromModules<Modules>,
33
+ FunctionReference<any, "public", any, any>
34
+ >;
35
+
36
+ type ConvertComponentBoundary<T> =
37
+ T extends ConvexId<string>
38
+ ? string
39
+ : T extends readonly (infer Item)[]
40
+ ? Array<ConvertComponentBoundary<Item>>
41
+ : T extends object
42
+ ? { [Key in keyof T]: ConvertComponentBoundary<T[Key]> }
43
+ : T;
44
+
45
+ type ConvertComponentArgs<Args extends Record<string, any>> = {
46
+ [Key in keyof Args]: ConvertComponentBoundary<Args[Key]>;
47
+ };
48
+
49
+ type ToComponentApi<API, Name extends string | undefined> =
50
+ API extends FunctionReference<infer Type, any, infer Args, infer ReturnType>
51
+ ? FunctionReference<
52
+ Type,
53
+ "internal",
54
+ ConvertComponentArgs<Args>,
55
+ ConvertComponentBoundary<ReturnType>,
56
+ Name
57
+ >
58
+ : API extends object
59
+ ? { [Key in keyof API]: ToComponentApi<API[Key], Name> }
60
+ : never;
61
+
62
+ /**
63
+ * A utility for referencing a Convex component's exposed API.
64
+ *
65
+ * Useful when expecting a parameter like `components.myComponent`.
66
+ * Usage:
67
+ * ```ts
68
+ * async function myFunction(ctx: QueryCtx, component: ComponentApi) {
69
+ * return ctx.runQuery(component.someFile.someQuery, { ...args });
70
+ * }
71
+ * ```
72
+ */
73
+ export type ComponentApi<Name extends string | undefined = string | undefined> =
74
+ ToComponentApi<PublicApi, Name>;
@@ -0,0 +1,40 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated data model types.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type {
12
+ DataModelFromSchemaDefinition,
13
+ DocumentByName,
14
+ TableNamesInDataModel,
15
+ } from "convex/server";
16
+ import type { GenericId } from "convex/values";
17
+ import schema from "../schema.js";
18
+
19
+ /**
20
+ * The names of all of your Convex tables.
21
+ */
22
+ export type TableNames = TableNamesInDataModel<DataModel>;
23
+
24
+ /**
25
+ * The type of a document stored in Convex.
26
+ */
27
+ export type Doc<TableName extends TableNames> = DocumentByName<
28
+ DataModel,
29
+ TableName
30
+ >;
31
+
32
+ /**
33
+ * An identifier for a document in Convex.
34
+ */
35
+ export type Id<TableName extends TableNames> = GenericId<TableName>;
36
+
37
+ /**
38
+ * A type describing your Convex data model.
39
+ */
40
+ export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
@@ -0,0 +1,48 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated utilities for implementing server-side Convex query and mutation functions.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type {
12
+ ActionBuilder,
13
+ HttpActionBuilder,
14
+ MutationBuilder,
15
+ QueryBuilder,
16
+ GenericActionCtx,
17
+ GenericMutationCtx,
18
+ GenericQueryCtx,
19
+ GenericDatabaseReader,
20
+ GenericDatabaseWriter,
21
+ } from "convex/server";
22
+ import {
23
+ actionGeneric,
24
+ httpActionGeneric,
25
+ queryGeneric,
26
+ mutationGeneric,
27
+ internalActionGeneric,
28
+ internalMutationGeneric,
29
+ internalQueryGeneric,
30
+ } from "convex/server";
31
+ import type { DataModel } from "./dataModel.js";
32
+
33
+ export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
34
+ export const internalQuery: QueryBuilder<DataModel, "internal"> =
35
+ internalQueryGeneric;
36
+ export const mutation: MutationBuilder<DataModel, "public"> = mutationGeneric;
37
+ export const internalMutation: MutationBuilder<DataModel, "internal"> =
38
+ internalMutationGeneric;
39
+ export const action: ActionBuilder<DataModel, "public"> = actionGeneric;
40
+ export const internalAction: ActionBuilder<DataModel, "internal"> =
41
+ internalActionGeneric;
42
+ export const httpAction: HttpActionBuilder = httpActionGeneric;
43
+
44
+ export type QueryCtx = GenericQueryCtx<DataModel>;
45
+ export type MutationCtx = GenericMutationCtx<DataModel>;
46
+ export type ActionCtx = GenericActionCtx<DataModel>;
47
+ export type DatabaseReader = GenericDatabaseReader<DataModel>;
48
+ export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
@@ -0,0 +1,47 @@
1
+ import { convexTest } from "convex-test";
2
+ import { describe, expect, it } from "vitest";
3
+ import { api } from "./_generated/api";
4
+ import schema from "./schema";
5
+ import { modules } from "./test.setup";
6
+
7
+ describe("backfillJobs", () => {
8
+ it("returns the latest backfill job for a connection", async () => {
9
+ const t = convexTest(schema, modules);
10
+
11
+ const connectionId = await t.run(async (ctx) => {
12
+ return await ctx.db.insert("connections", {
13
+ userId: "user-1",
14
+ provider: "garmin",
15
+ accessToken: "token",
16
+ status: "active",
17
+ });
18
+ });
19
+
20
+ await t.run(async (ctx) => {
21
+ await ctx.db.insert("backfillJobs", {
22
+ connectionId,
23
+ userId: "user-1",
24
+ provider: "garmin",
25
+ dataType: "full",
26
+ status: "failed",
27
+ startedAt: 1,
28
+ completedAt: 2,
29
+ });
30
+ await ctx.db.insert("backfillJobs", {
31
+ connectionId,
32
+ userId: "user-1",
33
+ provider: "garmin",
34
+ dataType: "full",
35
+ status: "running",
36
+ startedAt: 3,
37
+ });
38
+ });
39
+
40
+ const job = await t.query(api.backfillJobs.getLatestByConnection, {
41
+ connectionId,
42
+ });
43
+
44
+ expect(job?.status).toBe("running");
45
+ expect(job?.startedAt).toBe(3);
46
+ });
47
+ });
@@ -0,0 +1,245 @@
1
+ import { v } from "convex/values";
2
+ import { internal } from "./_generated/api";
3
+ import { internalMutation, internalQuery, query } from "./_generated/server";
4
+ import { backfillStatus, providerName } from "./schema";
5
+ import { durableWorkflow } from "./workflowManager";
6
+
7
+ export const backfillSignal = v.object({
8
+ kind: v.union(v.literal("webhook"), v.literal("timeout")),
9
+ dataType: v.string(),
10
+ itemCount: v.optional(v.number()),
11
+ });
12
+
13
+ export const getActiveByConnection = internalQuery({
14
+ args: {
15
+ connectionId: v.id("connections"),
16
+ },
17
+ returns: v.union(v.any(), v.null()),
18
+ handler: async (ctx, args) => {
19
+ const jobs = await ctx.db
20
+ .query("backfillJobs")
21
+ .withIndex("by_connection", (idx) => idx.eq("connectionId", args.connectionId))
22
+ .order("desc")
23
+ .take(10);
24
+
25
+ return jobs.find((job) => job.status === "queued" || job.status === "running") ?? null;
26
+ },
27
+ });
28
+
29
+ export const getById = internalQuery({
30
+ args: {
31
+ backfillJobId: v.id("backfillJobs"),
32
+ },
33
+ returns: v.union(v.any(), v.null()),
34
+ handler: async (ctx, args) => {
35
+ return await ctx.db.get(args.backfillJobId);
36
+ },
37
+ });
38
+
39
+ export const getLatestByConnection = query({
40
+ args: {
41
+ connectionId: v.id("connections"),
42
+ },
43
+ returns: v.union(v.any(), v.null()),
44
+ handler: async (ctx, args) => {
45
+ return await ctx.db
46
+ .query("backfillJobs")
47
+ .withIndex("by_connection", (idx) => idx.eq("connectionId", args.connectionId))
48
+ .order("desc")
49
+ .first();
50
+ },
51
+ });
52
+
53
+ export const create = internalMutation({
54
+ args: {
55
+ connectionId: v.id("connections"),
56
+ userId: v.string(),
57
+ provider: providerName,
58
+ dataType: v.string(),
59
+ windowStart: v.number(),
60
+ windowEnd: v.number(),
61
+ },
62
+ returns: v.id("backfillJobs"),
63
+ handler: async (ctx, args) => {
64
+ return await ctx.db.insert("backfillJobs", {
65
+ connectionId: args.connectionId,
66
+ userId: args.userId,
67
+ provider: args.provider,
68
+ dataType: args.dataType,
69
+ status: "queued",
70
+ startedAt: Date.now(),
71
+ windowStart: args.windowStart,
72
+ windowEnd: args.windowEnd,
73
+ completedDataTypes: [],
74
+ currentAttempt: 0,
75
+ });
76
+ },
77
+ });
78
+
79
+ export const updateStatus = internalMutation({
80
+ args: {
81
+ backfillJobId: v.id("backfillJobs"),
82
+ status: backfillStatus,
83
+ workflowId: v.optional(v.string()),
84
+ error: v.optional(v.string()),
85
+ currentDataType: v.optional(v.string()),
86
+ currentAttempt: v.optional(v.number()),
87
+ currentEventId: v.optional(v.string()),
88
+ completedDataTypes: v.optional(v.array(v.string())),
89
+ lastHeartbeatAt: v.optional(v.number()),
90
+ },
91
+ handler: async (ctx, args) => {
92
+ const updates: Record<string, unknown> = {
93
+ status: args.status,
94
+ };
95
+ if (args.workflowId !== undefined) updates.workflowId = args.workflowId;
96
+ if (args.error !== undefined) updates.error = args.error;
97
+ if (args.currentDataType !== undefined) updates.currentDataType = args.currentDataType;
98
+ if (args.currentAttempt !== undefined) updates.currentAttempt = args.currentAttempt;
99
+ if (args.currentEventId !== undefined) updates.currentEventId = args.currentEventId;
100
+ if (args.completedDataTypes !== undefined) updates.completedDataTypes = args.completedDataTypes;
101
+ if (args.lastHeartbeatAt !== undefined) updates.lastHeartbeatAt = args.lastHeartbeatAt;
102
+ if (args.status === "completed" || args.status === "failed" || args.status === "canceled") {
103
+ updates.completedAt = Date.now();
104
+ updates.currentEventId = undefined;
105
+ }
106
+ await ctx.db.patch(args.backfillJobId, updates);
107
+ },
108
+ });
109
+
110
+ export const beginAwaitingType = internalMutation({
111
+ args: {
112
+ backfillJobId: v.id("backfillJobs"),
113
+ workflowId: v.string(),
114
+ dataType: v.string(),
115
+ attempt: v.number(),
116
+ },
117
+ returns: v.string(),
118
+ handler: async (ctx, args) => {
119
+ const eventId = await durableWorkflow.createEvent(ctx, {
120
+ workflowId: args.workflowId as never,
121
+ name: `garmin-backfill:${args.dataType}:${args.attempt}`,
122
+ });
123
+
124
+ await ctx.db.patch(args.backfillJobId, {
125
+ status: "running",
126
+ currentDataType: args.dataType,
127
+ currentAttempt: args.attempt,
128
+ currentEventId: eventId,
129
+ lastHeartbeatAt: Date.now(),
130
+ });
131
+
132
+ return eventId;
133
+ },
134
+ });
135
+
136
+ export const markTypeCompleted = internalMutation({
137
+ args: {
138
+ backfillJobId: v.id("backfillJobs"),
139
+ dataType: v.string(),
140
+ },
141
+ handler: async (ctx, args) => {
142
+ const job = await ctx.db.get(args.backfillJobId);
143
+ if (!job) return;
144
+
145
+ const completed = new Set(job.completedDataTypes ?? []);
146
+ completed.add(args.dataType);
147
+
148
+ await ctx.db.patch(args.backfillJobId, {
149
+ completedDataTypes: Array.from(completed),
150
+ currentDataType: undefined,
151
+ currentAttempt: undefined,
152
+ currentEventId: undefined,
153
+ lastHeartbeatAt: Date.now(),
154
+ });
155
+ },
156
+ });
157
+
158
+ export const scheduleTimeout = internalMutation({
159
+ args: {
160
+ backfillJobId: v.id("backfillJobs"),
161
+ eventId: v.string(),
162
+ dataType: v.string(),
163
+ delayMs: v.number(),
164
+ },
165
+ handler: async (ctx, args) => {
166
+ await ctx.scheduler.runAfter(args.delayMs, internal.backfillJobs.emitTimeout, {
167
+ backfillJobId: args.backfillJobId,
168
+ eventId: args.eventId,
169
+ dataType: args.dataType,
170
+ });
171
+ },
172
+ });
173
+
174
+ export const emitTimeout = internalMutation({
175
+ args: {
176
+ backfillJobId: v.id("backfillJobs"),
177
+ eventId: v.string(),
178
+ dataType: v.string(),
179
+ },
180
+ handler: async (ctx, args) => {
181
+ const job = await ctx.db.get(args.backfillJobId);
182
+ if (
183
+ !job ||
184
+ job.status !== "running" ||
185
+ job.currentEventId !== args.eventId ||
186
+ job.currentDataType !== args.dataType
187
+ ) {
188
+ return;
189
+ }
190
+
191
+ try {
192
+ await durableWorkflow.sendEvent(ctx, {
193
+ id: args.eventId as never,
194
+ value: {
195
+ kind: "timeout",
196
+ dataType: args.dataType,
197
+ },
198
+ });
199
+ } catch {
200
+ // Timeout events are best-effort. If the workflow already moved on,
201
+ // duplicate or stale sends should not fail the scheduler.
202
+ }
203
+ },
204
+ });
205
+
206
+ export const signalWebhookData = internalMutation({
207
+ args: {
208
+ connectionId: v.id("connections"),
209
+ dataType: v.string(),
210
+ itemCount: v.optional(v.number()),
211
+ },
212
+ handler: async (ctx, args) => {
213
+ const activeJob = await ctx.db
214
+ .query("backfillJobs")
215
+ .withIndex("by_connection", (idx) => idx.eq("connectionId", args.connectionId))
216
+ .order("desc")
217
+ .first();
218
+
219
+ if (
220
+ !activeJob ||
221
+ activeJob.status !== "running" ||
222
+ activeJob.currentDataType !== args.dataType ||
223
+ !activeJob.currentEventId
224
+ ) {
225
+ return;
226
+ }
227
+
228
+ try {
229
+ await durableWorkflow.sendEvent(ctx, {
230
+ id: activeJob.currentEventId as never,
231
+ value: {
232
+ kind: "webhook",
233
+ dataType: args.dataType,
234
+ itemCount: args.itemCount,
235
+ },
236
+ });
237
+ await ctx.db.patch(activeJob._id, {
238
+ currentEventId: undefined,
239
+ lastHeartbeatAt: Date.now(),
240
+ });
241
+ } catch {
242
+ // Duplicate Garmin webhook pushes should not fail ingestion.
243
+ }
244
+ },
245
+ });