@zykeco/sync-server 0.4.0 → 0.6.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 (69) hide show
  1. package/dist/app.d.ts +4 -0
  2. package/dist/app.js +56 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/db/schema.d.ts +1009 -0
  5. package/dist/db/schema.js +80 -1
  6. package/dist/db/schema.js.map +1 -1
  7. package/dist/env.d.ts +6 -0
  8. package/dist/env.js +9 -1
  9. package/dist/env.js.map +1 -1
  10. package/dist/index.js +2 -23
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/metric-types.d.ts +2 -0
  13. package/dist/mcp/metric-types.js +42 -0
  14. package/dist/mcp/metric-types.js.map +1 -0
  15. package/dist/mcp/oauth.d.ts +41 -0
  16. package/dist/mcp/oauth.js +90 -0
  17. package/dist/mcp/oauth.js.map +1 -0
  18. package/dist/mcp/server.d.ts +35 -0
  19. package/dist/mcp/server.js +94 -0
  20. package/dist/mcp/server.js.map +1 -0
  21. package/dist/mcp/timezones.d.ts +17 -0
  22. package/dist/mcp/timezones.js +32 -0
  23. package/dist/mcp/timezones.js.map +1 -0
  24. package/dist/mcp/tools.d.ts +4 -0
  25. package/dist/mcp/tools.js +236 -0
  26. package/dist/mcp/tools.js.map +1 -0
  27. package/dist/mcp/transforms.d.ts +14 -0
  28. package/dist/mcp/transforms.js +45 -0
  29. package/dist/mcp/transforms.js.map +1 -0
  30. package/dist/routes/daily-stress-burden.d.ts +3 -0
  31. package/dist/routes/daily-stress-burden.js +24 -0
  32. package/dist/routes/daily-stress-burden.js.map +1 -0
  33. package/dist/routes/heart-rate-minute.d.ts +3 -0
  34. package/dist/routes/heart-rate-minute.js +24 -0
  35. package/dist/routes/heart-rate-minute.js.map +1 -0
  36. package/dist/routes/hr-zone-history.d.ts +3 -0
  37. package/dist/routes/hr-zone-history.js +24 -0
  38. package/dist/routes/hr-zone-history.js.map +1 -0
  39. package/dist/routes/mcp.d.ts +9 -0
  40. package/dist/routes/mcp.js +96 -0
  41. package/dist/routes/mcp.js.map +1 -0
  42. package/dist/routes/user-profile.d.ts +3 -0
  43. package/dist/routes/user-profile.js +24 -0
  44. package/dist/routes/user-profile.js.map +1 -0
  45. package/dist/routes/user-timezones.d.ts +3 -0
  46. package/dist/routes/user-timezones.js +24 -0
  47. package/dist/routes/user-timezones.js.map +1 -0
  48. package/dist/routes/wipe.d.ts +6 -1
  49. package/dist/routes/wipe.js +16 -2
  50. package/dist/routes/wipe.js.map +1 -1
  51. package/dist/stores/daily-stress-burden.d.ts +3 -0
  52. package/dist/stores/daily-stress-burden.js +58 -0
  53. package/dist/stores/daily-stress-burden.js.map +1 -0
  54. package/dist/stores/heart-rate-minute.d.ts +3 -0
  55. package/dist/stores/heart-rate-minute.js +48 -0
  56. package/dist/stores/heart-rate-minute.js.map +1 -0
  57. package/dist/stores/hr-zone-history.d.ts +3 -0
  58. package/dist/stores/hr-zone-history.js +49 -0
  59. package/dist/stores/hr-zone-history.js.map +1 -0
  60. package/dist/stores/user-profile.d.ts +3 -0
  61. package/dist/stores/user-profile.js +76 -0
  62. package/dist/stores/user-profile.js.map +1 -0
  63. package/dist/stores/user-timezones.d.ts +3 -0
  64. package/dist/stores/user-timezones.js +43 -0
  65. package/dist/stores/user-timezones.js.map +1 -0
  66. package/drizzle/0001_sweet_firebird.sql +78 -0
  67. package/drizzle/meta/0001_snapshot.json +733 -0
  68. package/drizzle/meta/_journal.json +7 -0
  69. package/package.json +4 -4
@@ -8,13 +8,27 @@ export function wipeRoutes(stores, auth) {
8
8
  const body = await parseJsonBody(c, WipeRequest);
9
9
  if (body instanceof Response)
10
10
  return body;
11
- const [dailyMetrics, weeklyMetrics, sleepSessions] = await Promise.all([
11
+ const [dailyMetrics, weeklyMetrics, sleepSessions, userTimezones, userProfile, dailyStressBurden, heartRateMinute, hrZoneHistory,] = await Promise.all([
12
12
  stores.dailyMetrics.wipeAll(),
13
13
  stores.weeklyMetrics.wipeAll(),
14
14
  stores.sleepSessions.wipeAll(),
15
+ stores.userTimezones.wipeAll(),
16
+ stores.userProfile.wipeAll(),
17
+ stores.dailyStressBurden.wipeAll(),
18
+ stores.heartRateMinute.wipeAll(),
19
+ stores.hrZoneHistory.wipeAll(),
15
20
  ]);
16
21
  return c.json({
17
- wiped: { dailyMetrics, weeklyMetrics, sleepSessions },
22
+ wiped: {
23
+ dailyMetrics,
24
+ weeklyMetrics,
25
+ sleepSessions,
26
+ userTimezones,
27
+ userProfile,
28
+ dailyStressBurden,
29
+ heartRateMinute,
30
+ hrZoneHistory,
31
+ },
18
32
  serverNowMs: Date.now(),
19
33
  });
20
34
  });
@@ -1 +1 @@
1
- {"version":3,"file":"wipe.js","sourceRoot":"","sources":["../../src/routes/wipe.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,UAAU,UAAU,CACxB,MAIC,EACD,IAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACpD,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QACjD,IAAI,IAAI,YAAY,QAAQ;YAAE,OAAO,IAAI,CAAC;QAE1C,MAAM,CAAC,YAAY,EAAE,aAAa,EAAE,aAAa,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACrE,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;YAC7B,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;YAC9B,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;SAC/B,CAAC,CAAC;QAEH,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE;YACrD,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;SACxB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"wipe.js","sourceRoot":"","sources":["../../src/routes/wipe.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,UAAU,UAAU,CACxB,MASC,EACD,IAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACpD,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QACjD,IAAI,IAAI,YAAY,QAAQ;YAAE,OAAO,IAAI,CAAC;QAE1C,MAAM,CACJ,YAAY,EACZ,aAAa,EACb,aAAa,EACb,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,aAAa,EACd,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACpB,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;YAC7B,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;YAC9B,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;YAC9B,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;YAC9B,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE;YAC5B,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE;YAClC,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE;YAChC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE;SAC/B,CAAC,CAAC;QAEH,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,KAAK,EAAE;gBACL,YAAY;gBACZ,aAAa;gBACb,aAAa;gBACb,aAAa;gBACb,WAAW;gBACX,iBAAiB;gBACjB,eAAe;gBACf,aAAa;aACd;YACD,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;SACxB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { DailyStressBurdenStore } from '@zykeco/server-core';
2
+ import type { Db } from '../db/client.js';
3
+ export declare function createDailyStressBurdenStore(db: Db): DailyStressBurdenStore;
@@ -0,0 +1,58 @@
1
+ import { inArray, sql } from 'drizzle-orm';
2
+ import { dailyStressBurden } from '../db/schema.js';
3
+ export function createDailyStressBurdenStore(db) {
4
+ return {
5
+ async upsertBatch(rows, _serverNowMs) {
6
+ if (rows.length === 0)
7
+ return [];
8
+ const values = rows.map((r) => ({
9
+ id: r.id,
10
+ isoDate: r.isoDate,
11
+ timezone: r.timezone,
12
+ intervalsRelaxed: r.intervalsRelaxed,
13
+ intervalsBalanced: r.intervalsBalanced,
14
+ intervalsElevated: r.intervalsElevated,
15
+ intervalsHigh: r.intervalsHigh,
16
+ intervalsScored: r.intervalsScored,
17
+ meanScore: r.meanScore,
18
+ peakScore: r.peakScore,
19
+ peakScoreUtc: r.peakScoreUtc,
20
+ stressBurden: r.stressBurden,
21
+ updatedAt: r.updatedAt,
22
+ }));
23
+ await db
24
+ .insert(dailyStressBurden)
25
+ .values(values)
26
+ .onConflictDoUpdate({
27
+ target: dailyStressBurden.id,
28
+ set: {
29
+ isoDate: sql `excluded.iso_date`,
30
+ timezone: sql `excluded.timezone`,
31
+ intervalsRelaxed: sql `excluded.intervals_relaxed`,
32
+ intervalsBalanced: sql `excluded.intervals_balanced`,
33
+ intervalsElevated: sql `excluded.intervals_elevated`,
34
+ intervalsHigh: sql `excluded.intervals_high`,
35
+ intervalsScored: sql `excluded.intervals_scored`,
36
+ meanScore: sql `excluded.mean_score`,
37
+ peakScore: sql `excluded.peak_score`,
38
+ peakScoreUtc: sql `excluded.peak_score_utc`,
39
+ stressBurden: sql `excluded.stress_burden`,
40
+ updatedAt: sql `excluded.updated_at`,
41
+ },
42
+ where: sql `excluded.updated_at > ${dailyStressBurden.updatedAt}`,
43
+ });
44
+ return rows.map((r) => r.id);
45
+ },
46
+ async deleteBatch(ids) {
47
+ if (ids.length === 0)
48
+ return [];
49
+ await db.delete(dailyStressBurden).where(inArray(dailyStressBurden.id, [...ids]));
50
+ return [...ids];
51
+ },
52
+ async wipeAll() {
53
+ const result = await db.delete(dailyStressBurden);
54
+ return result.rowsAffected;
55
+ },
56
+ };
57
+ }
58
+ //# sourceMappingURL=daily-stress-burden.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daily-stress-burden.js","sourceRoot":"","sources":["../../src/stores/daily-stress-burden.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,UAAU,4BAA4B,CAAC,EAAM;IACjD,OAAO;QACL,KAAK,CAAC,WAAW,CACf,IAAuC,EACvC,YAAoB;YAEpB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,gBAAgB,EAAE,CAAC,CAAC,gBAAgB;gBACpC,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;gBACtC,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;gBACtC,aAAa,EAAE,CAAC,CAAC,aAAa;gBAC9B,eAAe,EAAE,CAAC,CAAC,eAAe;gBAClC,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,YAAY,EAAE,CAAC,CAAC,YAAY;gBAC5B,YAAY,EAAE,CAAC,CAAC,YAAY;gBAC5B,SAAS,EAAE,CAAC,CAAC,SAAS;aACvB,CAAC,CAAC,CAAC;YACJ,MAAM,EAAE;iBACL,MAAM,CAAC,iBAAiB,CAAC;iBACzB,MAAM,CAAC,MAAM,CAAC;iBACd,kBAAkB,CAAC;gBAClB,MAAM,EAAE,iBAAiB,CAAC,EAAE;gBAC5B,GAAG,EAAE;oBACH,OAAO,EAAE,GAAG,CAAA,mBAAmB;oBAC/B,QAAQ,EAAE,GAAG,CAAA,mBAAmB;oBAChC,gBAAgB,EAAE,GAAG,CAAA,4BAA4B;oBACjD,iBAAiB,EAAE,GAAG,CAAA,6BAA6B;oBACnD,iBAAiB,EAAE,GAAG,CAAA,6BAA6B;oBACnD,aAAa,EAAE,GAAG,CAAA,yBAAyB;oBAC3C,eAAe,EAAE,GAAG,CAAA,2BAA2B;oBAC/C,SAAS,EAAE,GAAG,CAAA,qBAAqB;oBACnC,SAAS,EAAE,GAAG,CAAA,qBAAqB;oBACnC,YAAY,EAAE,GAAG,CAAA,yBAAyB;oBAC1C,YAAY,EAAE,GAAG,CAAA,wBAAwB;oBACzC,SAAS,EAAE,GAAG,CAAA,qBAAqB;iBACpC;gBACD,KAAK,EAAE,GAAG,CAAA,yBAAyB,iBAAiB,CAAC,SAAS,EAAE;aACjE,CAAC,CAAC;YACL,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,GAAsB;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAClF,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,KAAK,CAAC,OAAO;YACX,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;YAClD,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { HeartRateMinuteStore } from '@zykeco/server-core';
2
+ import type { Db } from '../db/client.js';
3
+ export declare function createHeartRateMinuteStore(db: Db): HeartRateMinuteStore;
@@ -0,0 +1,48 @@
1
+ import { inArray, sql } from 'drizzle-orm';
2
+ import { heartRateMinute } from '../db/schema.js';
3
+ export function createHeartRateMinuteStore(db) {
4
+ return {
5
+ async upsertBatch(rows, _serverNowMs) {
6
+ if (rows.length === 0)
7
+ return [];
8
+ const values = rows.map((r) => ({
9
+ id: r.id,
10
+ minuteStartUtc: r.minuteStartUtc,
11
+ bpmMean: r.bpmMean,
12
+ bpmMin: r.bpmMin,
13
+ bpmMax: r.bpmMax,
14
+ sampleCount: r.sampleCount,
15
+ deviceId: r.deviceId,
16
+ updatedAt: r.updatedAt,
17
+ }));
18
+ await db
19
+ .insert(heartRateMinute)
20
+ .values(values)
21
+ .onConflictDoUpdate({
22
+ target: heartRateMinute.id,
23
+ set: {
24
+ minuteStartUtc: sql `excluded.minute_start_utc`,
25
+ bpmMean: sql `excluded.bpm_mean`,
26
+ bpmMin: sql `excluded.bpm_min`,
27
+ bpmMax: sql `excluded.bpm_max`,
28
+ sampleCount: sql `excluded.sample_count`,
29
+ deviceId: sql `excluded.device_id`,
30
+ updatedAt: sql `excluded.updated_at`,
31
+ },
32
+ where: sql `excluded.updated_at > ${heartRateMinute.updatedAt}`,
33
+ });
34
+ return rows.map((r) => r.id);
35
+ },
36
+ async deleteBatch(ids) {
37
+ if (ids.length === 0)
38
+ return [];
39
+ await db.delete(heartRateMinute).where(inArray(heartRateMinute.id, [...ids]));
40
+ return [...ids];
41
+ },
42
+ async wipeAll() {
43
+ const result = await db.delete(heartRateMinute);
44
+ return result.rowsAffected;
45
+ },
46
+ };
47
+ }
48
+ //# sourceMappingURL=heart-rate-minute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heart-rate-minute.js","sourceRoot":"","sources":["../../src/stores/heart-rate-minute.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,UAAU,0BAA0B,CAAC,EAAM;IAC/C,OAAO;QACL,KAAK,CAAC,WAAW,CACf,IAAqC,EACrC,YAAoB;YAEpB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,cAAc,EAAE,CAAC,CAAC,cAAc;gBAChC,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,SAAS,EAAE,CAAC,CAAC,SAAS;aACvB,CAAC,CAAC,CAAC;YACJ,MAAM,EAAE;iBACL,MAAM,CAAC,eAAe,CAAC;iBACvB,MAAM,CAAC,MAAM,CAAC;iBACd,kBAAkB,CAAC;gBAClB,MAAM,EAAE,eAAe,CAAC,EAAE;gBAC1B,GAAG,EAAE;oBACH,cAAc,EAAE,GAAG,CAAA,2BAA2B;oBAC9C,OAAO,EAAE,GAAG,CAAA,mBAAmB;oBAC/B,MAAM,EAAE,GAAG,CAAA,kBAAkB;oBAC7B,MAAM,EAAE,GAAG,CAAA,kBAAkB;oBAC7B,WAAW,EAAE,GAAG,CAAA,uBAAuB;oBACvC,QAAQ,EAAE,GAAG,CAAA,oBAAoB;oBACjC,SAAS,EAAE,GAAG,CAAA,qBAAqB;iBACpC;gBACD,KAAK,EAAE,GAAG,CAAA,yBAAyB,eAAe,CAAC,SAAS,EAAE;aAC/D,CAAC,CAAC;YACL,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,GAAsB;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAC9E,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,KAAK,CAAC,OAAO;YACX,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YAChD,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { HrZoneHistoryStore } from '@zykeco/server-core';
2
+ import type { Db } from '../db/client.js';
3
+ export declare function createHrZoneHistoryStore(db: Db): HrZoneHistoryStore;
@@ -0,0 +1,49 @@
1
+ import { inArray, sql } from 'drizzle-orm';
2
+ import { hrZoneHistory } from '../db/schema.js';
3
+ export function createHrZoneHistoryStore(db) {
4
+ return {
5
+ async upsertBatch(rows, _serverNowMs) {
6
+ if (rows.length === 0)
7
+ return [];
8
+ const values = rows.map((r) => ({
9
+ id: r.id,
10
+ effectiveFromIsoDate: r.effectiveFromIsoDate,
11
+ zone1: r.zone1,
12
+ zone2: r.zone2,
13
+ zone3: r.zone3,
14
+ zone4: r.zone4,
15
+ zone5: r.zone5,
16
+ createdAt: r.createdAt,
17
+ }));
18
+ await db
19
+ .insert(hrZoneHistory)
20
+ .values(values)
21
+ .onConflictDoUpdate({
22
+ target: hrZoneHistory.id,
23
+ set: {
24
+ effectiveFromIsoDate: sql `excluded.effective_from_iso_date`,
25
+ zone1: sql `excluded.zone1`,
26
+ zone2: sql `excluded.zone2`,
27
+ zone3: sql `excluded.zone3`,
28
+ zone4: sql `excluded.zone4`,
29
+ zone5: sql `excluded.zone5`,
30
+ createdAt: sql `excluded.created_at`,
31
+ },
32
+ // LWW on createdAt (table is append-only; no updatedAt)
33
+ where: sql `excluded.created_at > ${hrZoneHistory.createdAt}`,
34
+ });
35
+ return rows.map((r) => r.id);
36
+ },
37
+ async deleteBatch(ids) {
38
+ if (ids.length === 0)
39
+ return [];
40
+ await db.delete(hrZoneHistory).where(inArray(hrZoneHistory.id, [...ids]));
41
+ return [...ids];
42
+ },
43
+ async wipeAll() {
44
+ const result = await db.delete(hrZoneHistory);
45
+ return result.rowsAffected;
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=hr-zone-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hr-zone-history.js","sourceRoot":"","sources":["../../src/stores/hr-zone-history.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,UAAU,wBAAwB,CAAC,EAAM;IAC7C,OAAO;QACL,KAAK,CAAC,WAAW,CACf,IAAmC,EACnC,YAAoB;YAEpB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,oBAAoB,EAAE,CAAC,CAAC,oBAAoB;gBAC5C,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,SAAS,EAAE,CAAC,CAAC,SAAS;aACvB,CAAC,CAAC,CAAC;YACJ,MAAM,EAAE;iBACL,MAAM,CAAC,aAAa,CAAC;iBACrB,MAAM,CAAC,MAAM,CAAC;iBACd,kBAAkB,CAAC;gBAClB,MAAM,EAAE,aAAa,CAAC,EAAE;gBACxB,GAAG,EAAE;oBACH,oBAAoB,EAAE,GAAG,CAAA,kCAAkC;oBAC3D,KAAK,EAAE,GAAG,CAAA,gBAAgB;oBAC1B,KAAK,EAAE,GAAG,CAAA,gBAAgB;oBAC1B,KAAK,EAAE,GAAG,CAAA,gBAAgB;oBAC1B,KAAK,EAAE,GAAG,CAAA,gBAAgB;oBAC1B,KAAK,EAAE,GAAG,CAAA,gBAAgB;oBAC1B,SAAS,EAAE,GAAG,CAAA,qBAAqB;iBACpC;gBACD,wDAAwD;gBACxD,KAAK,EAAE,GAAG,CAAA,yBAAyB,aAAa,CAAC,SAAS,EAAE;aAC7D,CAAC,CAAC;YACL,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,GAAsB;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1E,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,KAAK,CAAC,OAAO;YACX,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAC9C,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { UserProfileStore } from '@zykeco/server-core';
2
+ import type { Db } from '../db/client.js';
3
+ export declare function createUserProfileStore(db: Db): UserProfileStore;
@@ -0,0 +1,76 @@
1
+ import { inArray, sql } from 'drizzle-orm';
2
+ import { userProfile } from '../db/schema.js';
3
+ export function createUserProfileStore(db) {
4
+ return {
5
+ async upsertBatch(rows, _serverNowMs) {
6
+ if (rows.length === 0)
7
+ return [];
8
+ const values = rows.map((r) => ({
9
+ id: r.id,
10
+ name: r.name,
11
+ birthDate: r.birthDate,
12
+ height: r.height,
13
+ weight: r.weight,
14
+ biologicalGender: r.biologicalGender,
15
+ maxHeartRate: r.maxHeartRate,
16
+ maxHeartRateManually: r.maxHeartRateManually,
17
+ restingHeartRate: r.restingHeartRate,
18
+ vo2Max: r.vo2Max,
19
+ heartRateZonesManually: r.heartRateZonesManually,
20
+ hrZone1: r.hrZone1,
21
+ hrZone2: r.hrZone2,
22
+ hrZone3: r.hrZone3,
23
+ hrZone4: r.hrZone4,
24
+ hrZone5: r.hrZone5,
25
+ trainingBackground: r.trainingBackground,
26
+ typicalDay: r.typicalDay,
27
+ sleepGoalMinutes: r.sleepGoalMinutes,
28
+ dataRetentionLevel: r.dataRetentionLevel,
29
+ createdAt: r.createdAt,
30
+ updatedAt: r.updatedAt,
31
+ }));
32
+ await db
33
+ .insert(userProfile)
34
+ .values(values)
35
+ .onConflictDoUpdate({
36
+ target: userProfile.id,
37
+ set: {
38
+ name: sql `excluded.name`,
39
+ birthDate: sql `excluded.birth_date`,
40
+ height: sql `excluded.height`,
41
+ weight: sql `excluded.weight`,
42
+ biologicalGender: sql `excluded.biological_gender`,
43
+ maxHeartRate: sql `excluded.max_heart_rate`,
44
+ maxHeartRateManually: sql `excluded.max_heart_rate_manually`,
45
+ restingHeartRate: sql `excluded.resting_heart_rate`,
46
+ vo2Max: sql `excluded.vo2_max`,
47
+ heartRateZonesManually: sql `excluded.heart_rate_zones_manually`,
48
+ hrZone1: sql `excluded.hr_zone_1`,
49
+ hrZone2: sql `excluded.hr_zone_2`,
50
+ hrZone3: sql `excluded.hr_zone_3`,
51
+ hrZone4: sql `excluded.hr_zone_4`,
52
+ hrZone5: sql `excluded.hr_zone_5`,
53
+ trainingBackground: sql `excluded.training_background`,
54
+ typicalDay: sql `excluded.typical_day`,
55
+ sleepGoalMinutes: sql `excluded.sleep_goal_minutes`,
56
+ dataRetentionLevel: sql `excluded.data_retention_level`,
57
+ createdAt: sql `excluded.created_at`,
58
+ updatedAt: sql `excluded.updated_at`,
59
+ },
60
+ where: sql `excluded.updated_at > ${userProfile.updatedAt}`,
61
+ });
62
+ return rows.map((r) => r.id);
63
+ },
64
+ async deleteBatch(ids) {
65
+ if (ids.length === 0)
66
+ return [];
67
+ await db.delete(userProfile).where(inArray(userProfile.id, [...ids]));
68
+ return [...ids];
69
+ },
70
+ async wipeAll() {
71
+ const result = await db.delete(userProfile);
72
+ return result.rowsAffected;
73
+ },
74
+ };
75
+ }
76
+ //# sourceMappingURL=user-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-profile.js","sourceRoot":"","sources":["../../src/stores/user-profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { UserTimezonesStore } from '@zykeco/server-core';
2
+ import type { Db } from '../db/client.js';
3
+ export declare function createUserTimezonesStore(db: Db): UserTimezonesStore;
@@ -0,0 +1,43 @@
1
+ import { inArray, sql } from 'drizzle-orm';
2
+ import { userTimezones } from '../db/schema.js';
3
+ export function createUserTimezonesStore(db) {
4
+ return {
5
+ async upsertBatch(rows, _serverNowMs) {
6
+ if (rows.length === 0)
7
+ return [];
8
+ const values = rows.map((r) => ({
9
+ id: r.id,
10
+ timezone: r.timezone,
11
+ type: r.type,
12
+ utcOffsetMinutes: r.utcOffsetMinutes,
13
+ effectiveFrom: r.effectiveFrom,
14
+ }));
15
+ await db
16
+ .insert(userTimezones)
17
+ .values(values)
18
+ .onConflictDoUpdate({
19
+ target: userTimezones.id,
20
+ set: {
21
+ timezone: sql `excluded.timezone`,
22
+ type: sql `excluded.type`,
23
+ utcOffsetMinutes: sql `excluded.utc_offset_minutes`,
24
+ effectiveFrom: sql `excluded.effective_from`,
25
+ },
26
+ // LWW on effectiveFrom (table is append-only; no updatedAt)
27
+ where: sql `excluded.effective_from > ${userTimezones.effectiveFrom}`,
28
+ });
29
+ return rows.map((r) => r.id);
30
+ },
31
+ async deleteBatch(ids) {
32
+ if (ids.length === 0)
33
+ return [];
34
+ await db.delete(userTimezones).where(inArray(userTimezones.id, [...ids]));
35
+ return [...ids];
36
+ },
37
+ async wipeAll() {
38
+ const result = await db.delete(userTimezones);
39
+ return result.rowsAffected;
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=user-timezones.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-timezones.js","sourceRoot":"","sources":["../../src/stores/user-timezones.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,UAAU,wBAAwB,CAAC,EAAM;IAC7C,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,IAAkC,EAAE,YAAoB;YACxE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,gBAAgB,EAAE,CAAC,CAAC,gBAAgB;gBACpC,aAAa,EAAE,CAAC,CAAC,aAAa;aAC/B,CAAC,CAAC,CAAC;YACJ,MAAM,EAAE;iBACL,MAAM,CAAC,aAAa,CAAC;iBACrB,MAAM,CAAC,MAAM,CAAC;iBACd,kBAAkB,CAAC;gBAClB,MAAM,EAAE,aAAa,CAAC,EAAE;gBACxB,GAAG,EAAE;oBACH,QAAQ,EAAE,GAAG,CAAA,mBAAmB;oBAChC,IAAI,EAAE,GAAG,CAAA,eAAe;oBACxB,gBAAgB,EAAE,GAAG,CAAA,6BAA6B;oBAClD,aAAa,EAAE,GAAG,CAAA,yBAAyB;iBAC5C;gBACD,4DAA4D;gBAC5D,KAAK,EAAE,GAAG,CAAA,6BAA6B,aAAa,CAAC,aAAa,EAAE;aACrE,CAAC,CAAC;YACL,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC/B,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,GAAsB;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YAChC,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1E,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,KAAK,CAAC,OAAO;YACX,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAC9C,OAAO,MAAM,CAAC,YAAY,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,78 @@
1
+ CREATE TABLE `daily_stress_burden` (
2
+ `id` integer PRIMARY KEY NOT NULL,
3
+ `iso_date` text NOT NULL,
4
+ `timezone` text NOT NULL,
5
+ `intervals_relaxed` integer NOT NULL,
6
+ `intervals_balanced` integer NOT NULL,
7
+ `intervals_elevated` integer NOT NULL,
8
+ `intervals_high` integer NOT NULL,
9
+ `intervals_scored` integer NOT NULL,
10
+ `mean_score` real NOT NULL,
11
+ `peak_score` real NOT NULL,
12
+ `peak_score_utc` integer NOT NULL,
13
+ `stress_burden` real NOT NULL,
14
+ `updated_at` integer NOT NULL
15
+ );
16
+ --> statement-breakpoint
17
+ CREATE UNIQUE INDEX `dsb_unique_date_idx` ON `daily_stress_burden` (`iso_date`);--> statement-breakpoint
18
+ CREATE TABLE `heart_rate_minute` (
19
+ `id` integer PRIMARY KEY NOT NULL,
20
+ `minute_start_utc` integer NOT NULL,
21
+ `bpm_mean` real NOT NULL,
22
+ `bpm_min` integer NOT NULL,
23
+ `bpm_max` integer NOT NULL,
24
+ `sample_count` integer NOT NULL,
25
+ `device_id` integer NOT NULL,
26
+ `updated_at` integer NOT NULL
27
+ );
28
+ --> statement-breakpoint
29
+ CREATE INDEX `hrm_minute_idx` ON `heart_rate_minute` (`minute_start_utc`);--> statement-breakpoint
30
+ CREATE UNIQUE INDEX `hrm_unique_minute_per_device_idx` ON `heart_rate_minute` (`minute_start_utc`,`device_id`);--> statement-breakpoint
31
+ CREATE INDEX `hrm_device_updated_idx` ON `heart_rate_minute` (`device_id`,`updated_at`);--> statement-breakpoint
32
+ CREATE TABLE `hr_zone_history` (
33
+ `id` integer PRIMARY KEY NOT NULL,
34
+ `effective_from_iso_date` text NOT NULL,
35
+ `zone1` integer NOT NULL,
36
+ `zone2` integer NOT NULL,
37
+ `zone3` integer NOT NULL,
38
+ `zone4` integer NOT NULL,
39
+ `zone5` integer NOT NULL,
40
+ `created_at` integer NOT NULL
41
+ );
42
+ --> statement-breakpoint
43
+ CREATE UNIQUE INDEX `hrzh_unique_effective_idx` ON `hr_zone_history` (`effective_from_iso_date`);--> statement-breakpoint
44
+ CREATE TABLE `user_profile` (
45
+ `id` integer PRIMARY KEY NOT NULL,
46
+ `name` text NOT NULL,
47
+ `birth_date` text NOT NULL,
48
+ `height` real NOT NULL,
49
+ `weight` real NOT NULL,
50
+ `biological_gender` text NOT NULL,
51
+ `max_heart_rate` integer,
52
+ `max_heart_rate_manually` integer,
53
+ `resting_heart_rate` integer,
54
+ `vo2_max` real,
55
+ `heart_rate_zones_manually` integer,
56
+ `hr_zone_1` integer,
57
+ `hr_zone_2` integer,
58
+ `hr_zone_3` integer,
59
+ `hr_zone_4` integer,
60
+ `hr_zone_5` integer,
61
+ `training_background` text,
62
+ `typical_day` text,
63
+ `sleep_goal_minutes` integer,
64
+ `data_retention_level` text,
65
+ `created_at` integer NOT NULL,
66
+ `updated_at` integer NOT NULL,
67
+ CONSTRAINT "user_profile_singleton" CHECK("user_profile"."id" = 1)
68
+ );
69
+ --> statement-breakpoint
70
+ CREATE TABLE `user_timezones` (
71
+ `id` integer PRIMARY KEY NOT NULL,
72
+ `timezone` text NOT NULL,
73
+ `type` text NOT NULL,
74
+ `utc_offset_minutes` integer NOT NULL,
75
+ `effective_from` integer NOT NULL
76
+ );
77
+ --> statement-breakpoint
78
+ CREATE INDEX `user_tz_effective_from_idx` ON `user_timezones` (`effective_from`);