@clipin/convex-wearables 0.1.3 → 0.2.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 +11 -2
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +6 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/component/garminWebhooks.js +2 -0
- package/dist/component/garminWebhooks.js.map +1 -1
- package/dist/component/providers/types.d.ts +2 -0
- package/dist/component/providers/types.d.ts.map +1 -1
- package/dist/component/schema.d.ts +11 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +6 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/sdkPush.d.ts +4 -0
- package/dist/component/sdkPush.d.ts.map +1 -1
- package/dist/component/sdkPush.js +5 -0
- package/dist/component/sdkPush.js.map +1 -1
- package/dist/component/summaries.d.ts +16 -1
- package/dist/component/summaries.d.ts.map +1 -1
- package/dist/component/summaries.js +72 -38
- package/dist/component/summaries.js.map +1 -1
- package/dist/component/syncWorkflow.d.ts.map +1 -1
- package/dist/component/syncWorkflow.js +2 -0
- package/dist/component/syncWorkflow.js.map +1 -1
- package/dist/test.d.ts +11 -1
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/client/types.ts +6 -0
- package/src/component/garminWebhooks.test.ts +2 -0
- package/src/component/garminWebhooks.ts +2 -0
- package/src/component/providers/types.ts +2 -0
- package/src/component/schema.ts +6 -0
- package/src/component/sdkPush.test.ts +83 -0
- package/src/component/sdkPush.ts +5 -0
- package/src/component/summaries.test.ts +99 -0
- package/src/component/summaries.ts +89 -39
- package/src/component/syncWorkflow.ts +2 -0
package/src/component/schema.ts
CHANGED
|
@@ -200,6 +200,10 @@ export default defineSchema({
|
|
|
200
200
|
// -------------------------------------------------------------------------
|
|
201
201
|
dailySummaries: defineTable({
|
|
202
202
|
userId: v.string(),
|
|
203
|
+
provider: v.optional(providerName),
|
|
204
|
+
dataSourceId: v.optional(v.id("dataSources")),
|
|
205
|
+
source: v.optional(v.string()),
|
|
206
|
+
originalSourceName: v.optional(v.string()),
|
|
203
207
|
date: v.string(), // "2026-03-15" (ISO date string)
|
|
204
208
|
category: v.string(), // "activity" | "sleep" | "recovery" | "body"
|
|
205
209
|
|
|
@@ -241,6 +245,8 @@ export default defineSchema({
|
|
|
241
245
|
bodyBattery: v.optional(v.number()),
|
|
242
246
|
spo2Avg: v.optional(v.number()),
|
|
243
247
|
})
|
|
248
|
+
.index("by_user_provider_category_date", ["userId", "provider", "category", "date"])
|
|
249
|
+
.index("by_user_provider_date", ["userId", "provider", "date"])
|
|
244
250
|
.index("by_user_category_date", ["userId", "category", "date"])
|
|
245
251
|
.index("by_user_date", ["userId", "date"]),
|
|
246
252
|
|
|
@@ -130,6 +130,20 @@ describe("sdkPush", () => {
|
|
|
130
130
|
.collect();
|
|
131
131
|
});
|
|
132
132
|
expect(summaries).toHaveLength(2);
|
|
133
|
+
expect(summaries).toEqual(
|
|
134
|
+
expect.arrayContaining([
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
provider: "google",
|
|
137
|
+
source: "health-connect",
|
|
138
|
+
category: "activity",
|
|
139
|
+
}),
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
provider: "google",
|
|
142
|
+
source: "health-connect",
|
|
143
|
+
category: "recovery",
|
|
144
|
+
}),
|
|
145
|
+
]),
|
|
146
|
+
);
|
|
133
147
|
});
|
|
134
148
|
|
|
135
149
|
it("deduplicates SDK pushes by external id and source-time keys", async () => {
|
|
@@ -304,12 +318,81 @@ describe("sdkPush", () => {
|
|
|
304
318
|
|
|
305
319
|
expect(summaries).toHaveLength(1);
|
|
306
320
|
expect(summaries[0]).toMatchObject({
|
|
321
|
+
provider: "google",
|
|
322
|
+
source: "health-connect",
|
|
307
323
|
category: "activity",
|
|
308
324
|
totalSteps: 12345,
|
|
309
325
|
totalCalories: 780,
|
|
310
326
|
});
|
|
311
327
|
});
|
|
312
328
|
|
|
329
|
+
it("keeps Apple and Google daily summaries separate for the same user date and category", async () => {
|
|
330
|
+
const t = convexTest(schema, modules);
|
|
331
|
+
|
|
332
|
+
await t.action(api.sdkPush.ingestNormalizedPayload, {
|
|
333
|
+
userId: "user-mixed",
|
|
334
|
+
provider: "apple",
|
|
335
|
+
sourceMetadata: {
|
|
336
|
+
source: "healthkit",
|
|
337
|
+
originalSourceName: "Apple Watch",
|
|
338
|
+
},
|
|
339
|
+
summaries: [
|
|
340
|
+
{
|
|
341
|
+
date: "2026-03-18",
|
|
342
|
+
category: "activity",
|
|
343
|
+
totalSteps: 9000,
|
|
344
|
+
activeCalories: 450,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await t.action(api.sdkPush.ingestNormalizedPayload, {
|
|
350
|
+
userId: "user-mixed",
|
|
351
|
+
provider: "google",
|
|
352
|
+
sourceMetadata: {
|
|
353
|
+
source: "health-connect",
|
|
354
|
+
originalSourceName: "com.google.android.apps.fitness",
|
|
355
|
+
},
|
|
356
|
+
summaries: [
|
|
357
|
+
{
|
|
358
|
+
date: "2026-03-18",
|
|
359
|
+
category: "activity",
|
|
360
|
+
totalSteps: 7200,
|
|
361
|
+
activeCalories: 330,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const summaries = await t.run(async (ctx) => {
|
|
367
|
+
return await ctx.db
|
|
368
|
+
.query("dailySummaries")
|
|
369
|
+
.withIndex("by_user_category_date", (idx) =>
|
|
370
|
+
idx.eq("userId", "user-mixed").eq("category", "activity").eq("date", "2026-03-18"),
|
|
371
|
+
)
|
|
372
|
+
.collect();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(summaries).toHaveLength(2);
|
|
376
|
+
expect(summaries).toEqual(
|
|
377
|
+
expect.arrayContaining([
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
provider: "apple",
|
|
380
|
+
source: "healthkit",
|
|
381
|
+
originalSourceName: "Apple Watch",
|
|
382
|
+
totalSteps: 9000,
|
|
383
|
+
activeCalories: 450,
|
|
384
|
+
}),
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
provider: "google",
|
|
387
|
+
source: "health-connect",
|
|
388
|
+
originalSourceName: "com.google.android.apps.fitness",
|
|
389
|
+
totalSteps: 7200,
|
|
390
|
+
activeCalories: 330,
|
|
391
|
+
}),
|
|
392
|
+
]),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
313
396
|
it("batches large data-point payloads across multiple writes", async () => {
|
|
314
397
|
const t = convexTest(schema, modules);
|
|
315
398
|
|
package/src/component/sdkPush.ts
CHANGED
|
@@ -93,6 +93,8 @@ const sdkDataPointValidator = v.object({
|
|
|
93
93
|
const sdkSummaryValidator = v.object({
|
|
94
94
|
date: v.string(),
|
|
95
95
|
category: v.string(),
|
|
96
|
+
source: v.optional(v.string()),
|
|
97
|
+
originalSourceName: v.optional(v.string()),
|
|
96
98
|
totalSteps: v.optional(v.number()),
|
|
97
99
|
totalCalories: v.optional(v.number()),
|
|
98
100
|
activeCalories: v.optional(v.number()),
|
|
@@ -360,7 +362,10 @@ export const ingestNormalizedPayload = action({
|
|
|
360
362
|
for (const summary of summaries) {
|
|
361
363
|
await ctx.runMutation(internal.summaries.upsert, {
|
|
362
364
|
userId: args.userId,
|
|
365
|
+
provider: args.provider,
|
|
363
366
|
...summary,
|
|
367
|
+
source: summary.source ?? defaultMetadata.source,
|
|
368
|
+
originalSourceName: summary.originalSourceName ?? defaultMetadata.originalSourceName,
|
|
364
369
|
});
|
|
365
370
|
}
|
|
366
371
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { convexTest } from "convex-test";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { api, internal } from "./_generated/api";
|
|
3
4
|
import schema from "./schema";
|
|
4
5
|
import { modules } from "./test.setup";
|
|
5
6
|
|
|
@@ -81,6 +82,62 @@ describe("summaries", () => {
|
|
|
81
82
|
expect(summaries[0].totalSteps).toBe(10000);
|
|
82
83
|
expect(summaries[0].activeMinutes).toBe(45);
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
it("upserts summaries by user provider category and date", async () => {
|
|
87
|
+
const t = convexTest(schema, modules);
|
|
88
|
+
|
|
89
|
+
await t.mutation(internal.summaries.upsert, {
|
|
90
|
+
userId: "user-1",
|
|
91
|
+
provider: "garmin",
|
|
92
|
+
date: "2026-03-15",
|
|
93
|
+
category: "activity",
|
|
94
|
+
totalSteps: 10000,
|
|
95
|
+
activeCalories: 600,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await t.mutation(internal.summaries.upsert, {
|
|
99
|
+
userId: "user-1",
|
|
100
|
+
provider: "apple",
|
|
101
|
+
date: "2026-03-15",
|
|
102
|
+
category: "activity",
|
|
103
|
+
source: "healthkit",
|
|
104
|
+
originalSourceName: "Apple Watch",
|
|
105
|
+
totalSteps: 8500,
|
|
106
|
+
activeCalories: 430,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await t.mutation(internal.summaries.upsert, {
|
|
110
|
+
userId: "user-1",
|
|
111
|
+
provider: "garmin",
|
|
112
|
+
date: "2026-03-15",
|
|
113
|
+
category: "activity",
|
|
114
|
+
totalSteps: 11000,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const summaries = await t.run(async (ctx) => {
|
|
118
|
+
return await ctx.db
|
|
119
|
+
.query("dailySummaries")
|
|
120
|
+
.withIndex("by_user_category_date", (idx) =>
|
|
121
|
+
idx.eq("userId", "user-1").eq("category", "activity").eq("date", "2026-03-15"),
|
|
122
|
+
)
|
|
123
|
+
.collect();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(summaries).toHaveLength(2);
|
|
127
|
+
const garmin = summaries.find((summary) => summary.provider === "garmin");
|
|
128
|
+
const apple = summaries.find((summary) => summary.provider === "apple");
|
|
129
|
+
|
|
130
|
+
expect(garmin).toMatchObject({
|
|
131
|
+
totalSteps: 11000,
|
|
132
|
+
activeCalories: 600,
|
|
133
|
+
});
|
|
134
|
+
expect(apple).toMatchObject({
|
|
135
|
+
source: "healthkit",
|
|
136
|
+
originalSourceName: "Apple Watch",
|
|
137
|
+
totalSteps: 8500,
|
|
138
|
+
activeCalories: 430,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
84
141
|
});
|
|
85
142
|
|
|
86
143
|
describe("getDailySummaries", () => {
|
|
@@ -156,6 +213,48 @@ describe("summaries", () => {
|
|
|
156
213
|
expect(sleep).toHaveLength(1);
|
|
157
214
|
expect(sleep[0].sleepDurationMinutes).toBe(480);
|
|
158
215
|
});
|
|
216
|
+
|
|
217
|
+
it("filters summaries by provider when requested", async () => {
|
|
218
|
+
const t = convexTest(schema, modules);
|
|
219
|
+
|
|
220
|
+
await t.mutation(internal.summaries.upsert, {
|
|
221
|
+
userId: "user-1",
|
|
222
|
+
provider: "garmin",
|
|
223
|
+
date: "2026-03-15",
|
|
224
|
+
category: "activity",
|
|
225
|
+
totalSteps: 10000,
|
|
226
|
+
});
|
|
227
|
+
await t.mutation(internal.summaries.upsert, {
|
|
228
|
+
userId: "user-1",
|
|
229
|
+
provider: "google",
|
|
230
|
+
date: "2026-03-15",
|
|
231
|
+
category: "activity",
|
|
232
|
+
source: "health-connect",
|
|
233
|
+
totalSteps: 9200,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const google = await t.query(api.summaries.getDailySummaries, {
|
|
237
|
+
userId: "user-1",
|
|
238
|
+
provider: "google",
|
|
239
|
+
category: "activity",
|
|
240
|
+
startDate: "2026-03-15",
|
|
241
|
+
endDate: "2026-03-15",
|
|
242
|
+
});
|
|
243
|
+
const mixed = await t.query(api.summaries.getDailySummaries, {
|
|
244
|
+
userId: "user-1",
|
|
245
|
+
category: "activity",
|
|
246
|
+
startDate: "2026-03-15",
|
|
247
|
+
endDate: "2026-03-15",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(google).toHaveLength(1);
|
|
251
|
+
expect(google[0]).toMatchObject({
|
|
252
|
+
provider: "google",
|
|
253
|
+
source: "health-connect",
|
|
254
|
+
totalSteps: 9200,
|
|
255
|
+
});
|
|
256
|
+
expect(mixed).toHaveLength(2);
|
|
257
|
+
});
|
|
159
258
|
});
|
|
160
259
|
|
|
161
260
|
describe("getByUserDate", () => {
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { internalMutation, internalQuery, query } from "./_generated/server";
|
|
3
|
+
import { providerName } from "./schema";
|
|
4
|
+
|
|
5
|
+
const summaryMetricsValidator = {
|
|
6
|
+
totalSteps: v.optional(v.number()),
|
|
7
|
+
totalCalories: v.optional(v.number()),
|
|
8
|
+
activeCalories: v.optional(v.number()),
|
|
9
|
+
activeMinutes: v.optional(v.number()),
|
|
10
|
+
totalDistance: v.optional(v.number()),
|
|
11
|
+
floorsClimbed: v.optional(v.number()),
|
|
12
|
+
avgHeartRate: v.optional(v.number()),
|
|
13
|
+
maxHeartRate: v.optional(v.number()),
|
|
14
|
+
minHeartRate: v.optional(v.number()),
|
|
15
|
+
sleepDurationMinutes: v.optional(v.number()),
|
|
16
|
+
sleepEfficiency: v.optional(v.number()),
|
|
17
|
+
deepSleepMinutes: v.optional(v.number()),
|
|
18
|
+
remSleepMinutes: v.optional(v.number()),
|
|
19
|
+
lightSleepMinutes: v.optional(v.number()),
|
|
20
|
+
awakeDuringMinutes: v.optional(v.number()),
|
|
21
|
+
timeInBedMinutes: v.optional(v.number()),
|
|
22
|
+
hrvAvg: v.optional(v.number()),
|
|
23
|
+
hrvRmssd: v.optional(v.number()),
|
|
24
|
+
restingHeartRate: v.optional(v.number()),
|
|
25
|
+
recoveryScore: v.optional(v.number()),
|
|
26
|
+
weight: v.optional(v.number()),
|
|
27
|
+
bodyFatPercentage: v.optional(v.number()),
|
|
28
|
+
bodyMassIndex: v.optional(v.number()),
|
|
29
|
+
leanBodyMass: v.optional(v.number()),
|
|
30
|
+
bodyTemperature: v.optional(v.number()),
|
|
31
|
+
avgStressLevel: v.optional(v.number()),
|
|
32
|
+
bodyBattery: v.optional(v.number()),
|
|
33
|
+
spo2Avg: v.optional(v.number()),
|
|
34
|
+
};
|
|
3
35
|
|
|
4
36
|
// ---------------------------------------------------------------------------
|
|
5
37
|
// Queries
|
|
@@ -7,17 +39,33 @@ import { internalMutation, internalQuery, query } from "./_generated/server";
|
|
|
7
39
|
|
|
8
40
|
/**
|
|
9
41
|
* Get daily summaries for a user by category and date range.
|
|
10
|
-
*
|
|
42
|
+
* When provider is omitted this returns provider-mixed storage rows and is not
|
|
43
|
+
* a canonical product view for multi-provider apps.
|
|
11
44
|
*/
|
|
12
45
|
export const getDailySummaries = query({
|
|
13
46
|
args: {
|
|
14
47
|
userId: v.string(),
|
|
48
|
+
provider: v.optional(providerName),
|
|
15
49
|
category: v.string(),
|
|
16
50
|
startDate: v.string(), // "2026-03-01"
|
|
17
51
|
endDate: v.string(), // "2026-03-15"
|
|
18
52
|
},
|
|
19
53
|
returns: v.array(v.any()),
|
|
20
54
|
handler: async (ctx, args) => {
|
|
55
|
+
if (args.provider !== undefined) {
|
|
56
|
+
return await ctx.db
|
|
57
|
+
.query("dailySummaries")
|
|
58
|
+
.withIndex("by_user_provider_category_date", (idx) =>
|
|
59
|
+
idx
|
|
60
|
+
.eq("userId", args.userId)
|
|
61
|
+
.eq("provider", args.provider)
|
|
62
|
+
.eq("category", args.category)
|
|
63
|
+
.gte("date", args.startDate)
|
|
64
|
+
.lte("date", args.endDate),
|
|
65
|
+
)
|
|
66
|
+
.collect();
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
return await ctx.db
|
|
22
70
|
.query("dailySummaries")
|
|
23
71
|
.withIndex("by_user_category_date", (idx) =>
|
|
@@ -37,10 +85,20 @@ export const getDailySummaries = query({
|
|
|
37
85
|
export const getByUserDate = internalQuery({
|
|
38
86
|
args: {
|
|
39
87
|
userId: v.string(),
|
|
88
|
+
provider: v.optional(providerName),
|
|
40
89
|
date: v.string(),
|
|
41
90
|
},
|
|
42
91
|
returns: v.array(v.any()),
|
|
43
92
|
handler: async (ctx, args) => {
|
|
93
|
+
if (args.provider !== undefined) {
|
|
94
|
+
return await ctx.db
|
|
95
|
+
.query("dailySummaries")
|
|
96
|
+
.withIndex("by_user_provider_date", (idx) =>
|
|
97
|
+
idx.eq("userId", args.userId).eq("provider", args.provider).eq("date", args.date),
|
|
98
|
+
)
|
|
99
|
+
.collect();
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
return await ctx.db
|
|
45
103
|
.query("dailySummaries")
|
|
46
104
|
.withIndex("by_user_date", (idx) => idx.eq("userId", args.userId).eq("date", args.date))
|
|
@@ -59,68 +117,60 @@ export const getByUserDate = internalQuery({
|
|
|
59
117
|
export const upsert = internalMutation({
|
|
60
118
|
args: {
|
|
61
119
|
userId: v.string(),
|
|
120
|
+
provider: providerName,
|
|
121
|
+
dataSourceId: v.optional(v.id("dataSources")),
|
|
122
|
+
source: v.optional(v.string()),
|
|
123
|
+
originalSourceName: v.optional(v.string()),
|
|
62
124
|
date: v.string(),
|
|
63
125
|
category: v.string(),
|
|
64
126
|
// All metric fields are optional — only provided fields are updated
|
|
65
|
-
|
|
66
|
-
totalCalories: v.optional(v.number()),
|
|
67
|
-
activeCalories: v.optional(v.number()),
|
|
68
|
-
activeMinutes: v.optional(v.number()),
|
|
69
|
-
totalDistance: v.optional(v.number()),
|
|
70
|
-
floorsClimbed: v.optional(v.number()),
|
|
71
|
-
avgHeartRate: v.optional(v.number()),
|
|
72
|
-
maxHeartRate: v.optional(v.number()),
|
|
73
|
-
minHeartRate: v.optional(v.number()),
|
|
74
|
-
sleepDurationMinutes: v.optional(v.number()),
|
|
75
|
-
sleepEfficiency: v.optional(v.number()),
|
|
76
|
-
deepSleepMinutes: v.optional(v.number()),
|
|
77
|
-
remSleepMinutes: v.optional(v.number()),
|
|
78
|
-
lightSleepMinutes: v.optional(v.number()),
|
|
79
|
-
awakeDuringMinutes: v.optional(v.number()),
|
|
80
|
-
timeInBedMinutes: v.optional(v.number()),
|
|
81
|
-
hrvAvg: v.optional(v.number()),
|
|
82
|
-
hrvRmssd: v.optional(v.number()),
|
|
83
|
-
restingHeartRate: v.optional(v.number()),
|
|
84
|
-
recoveryScore: v.optional(v.number()),
|
|
85
|
-
weight: v.optional(v.number()),
|
|
86
|
-
bodyFatPercentage: v.optional(v.number()),
|
|
87
|
-
bodyMassIndex: v.optional(v.number()),
|
|
88
|
-
leanBodyMass: v.optional(v.number()),
|
|
89
|
-
bodyTemperature: v.optional(v.number()),
|
|
90
|
-
avgStressLevel: v.optional(v.number()),
|
|
91
|
-
bodyBattery: v.optional(v.number()),
|
|
92
|
-
spo2Avg: v.optional(v.number()),
|
|
127
|
+
...summaryMetricsValidator,
|
|
93
128
|
},
|
|
94
129
|
returns: v.id("dailySummaries"),
|
|
95
130
|
handler: async (ctx, args) => {
|
|
96
|
-
const {
|
|
131
|
+
const {
|
|
132
|
+
userId,
|
|
133
|
+
provider,
|
|
134
|
+
dataSourceId,
|
|
135
|
+
source,
|
|
136
|
+
originalSourceName,
|
|
137
|
+
date,
|
|
138
|
+
category,
|
|
139
|
+
...metrics
|
|
140
|
+
} = args;
|
|
97
141
|
|
|
98
|
-
//
|
|
142
|
+
// New writes are provider-scoped. Legacy rows without provider remain
|
|
143
|
+
// readable through unfiltered queries but are not canonical for new ingest.
|
|
99
144
|
const existing = await ctx.db
|
|
100
145
|
.query("dailySummaries")
|
|
101
|
-
.withIndex("
|
|
102
|
-
idx.eq("userId", userId).eq("category", category).eq("date", date),
|
|
146
|
+
.withIndex("by_user_provider_category_date", (idx) =>
|
|
147
|
+
idx.eq("userId", userId).eq("provider", provider).eq("category", category).eq("date", date),
|
|
103
148
|
)
|
|
104
149
|
.first();
|
|
105
150
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
151
|
+
const definedFields: Record<string, unknown> = {};
|
|
152
|
+
for (const [key, value] of Object.entries({
|
|
153
|
+
dataSourceId,
|
|
154
|
+
source,
|
|
155
|
+
originalSourceName,
|
|
156
|
+
...metrics,
|
|
157
|
+
})) {
|
|
109
158
|
if (value !== undefined) {
|
|
110
|
-
|
|
159
|
+
definedFields[key] = value;
|
|
111
160
|
}
|
|
112
161
|
}
|
|
113
162
|
|
|
114
163
|
if (existing) {
|
|
115
|
-
await ctx.db.patch(existing._id,
|
|
164
|
+
await ctx.db.patch(existing._id, definedFields);
|
|
116
165
|
return existing._id;
|
|
117
166
|
}
|
|
118
167
|
|
|
119
168
|
return await ctx.db.insert("dailySummaries", {
|
|
120
169
|
userId,
|
|
170
|
+
provider,
|
|
121
171
|
date,
|
|
122
172
|
category,
|
|
123
|
-
...
|
|
173
|
+
...definedFields,
|
|
124
174
|
});
|
|
125
175
|
},
|
|
126
176
|
});
|
|
@@ -501,7 +501,9 @@ export const runConnectionSync = durableWorkflow.define({
|
|
|
501
501
|
for (const summary of batch.summaries as NormalizedDailySummary[]) {
|
|
502
502
|
await step.runMutation(internal.summaries.upsert, {
|
|
503
503
|
userId: job.userId,
|
|
504
|
+
provider: job.provider,
|
|
504
505
|
...summary,
|
|
506
|
+
source: summary.source ?? job.provider,
|
|
505
507
|
});
|
|
506
508
|
}
|
|
507
509
|
processed += batch.summaries.length;
|