@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,243 @@
|
|
|
1
|
+
import type { Doc } from "./_generated/dataModel";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_POLICY_SET_KEY = "__default__";
|
|
4
|
+
export const DEFAULT_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_TIME_SERIES_AGGREGATIONS = ["avg", "min", "max", "last", "count"] as const;
|
|
6
|
+
|
|
7
|
+
export type DurationInput = string | number;
|
|
8
|
+
export type TimeSeriesAggregation = Doc<"timeSeriesPolicyRules">["tiers"][number] extends infer Tier
|
|
9
|
+
? Tier extends { aggregations: infer Aggregations }
|
|
10
|
+
? Aggregations extends Array<infer Aggregation>
|
|
11
|
+
? Aggregation
|
|
12
|
+
: never
|
|
13
|
+
: never
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
export type TimeSeriesTierInput =
|
|
17
|
+
| {
|
|
18
|
+
kind: "raw";
|
|
19
|
+
fromAge: DurationInput;
|
|
20
|
+
toAge: DurationInput | null;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
kind: "rollup";
|
|
24
|
+
fromAge: DurationInput;
|
|
25
|
+
toAge: DurationInput | null;
|
|
26
|
+
bucket: DurationInput;
|
|
27
|
+
aggregations?: TimeSeriesAggregation[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type NormalizedTimeSeriesTier = Doc<"timeSeriesPolicyRules">["tiers"][number];
|
|
31
|
+
|
|
32
|
+
export type TimeSeriesPolicyRuleInput = {
|
|
33
|
+
provider?: Doc<"timeSeriesPolicyRules">["provider"];
|
|
34
|
+
seriesType?: string;
|
|
35
|
+
tiers: TimeSeriesTierInput[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DURATION_UNITS = {
|
|
39
|
+
ms: 1,
|
|
40
|
+
s: 1000,
|
|
41
|
+
m: 60 * 1000,
|
|
42
|
+
h: 60 * 60 * 1000,
|
|
43
|
+
d: 24 * 60 * 60 * 1000,
|
|
44
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export function parseDurationInput(input: DurationInput, label: string) {
|
|
48
|
+
if (typeof input === "number") {
|
|
49
|
+
if (!Number.isFinite(input) || input < 0) {
|
|
50
|
+
throw new Error(`${label} must be a non-negative duration`);
|
|
51
|
+
}
|
|
52
|
+
return Math.floor(input);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trimmed = input.trim().toLowerCase();
|
|
56
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/.exec(trimmed);
|
|
57
|
+
if (!match) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${label} must be a duration like "30m", "24h", "7d", or a numeric millisecond value`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const value = Number(match[1]);
|
|
64
|
+
const unit = match[2] as keyof typeof DURATION_UNITS;
|
|
65
|
+
return Math.floor(value * DURATION_UNITS[unit]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createTimeSeriesPolicyScopeKey(provider?: string, seriesType?: string) {
|
|
69
|
+
return `${provider ?? "*"}::${seriesType ?? "*"}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function inferTimeSeriesPolicyScope(provider?: string, seriesType?: string) {
|
|
73
|
+
if (provider && seriesType) {
|
|
74
|
+
return "provider_series" as const;
|
|
75
|
+
}
|
|
76
|
+
if (seriesType) {
|
|
77
|
+
return "series" as const;
|
|
78
|
+
}
|
|
79
|
+
if (provider) {
|
|
80
|
+
return "provider" as const;
|
|
81
|
+
}
|
|
82
|
+
return "global" as const;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeAggregations(aggregations?: readonly TimeSeriesAggregation[]) {
|
|
86
|
+
const unique = new Set(aggregations ?? (DEFAULT_TIME_SERIES_AGGREGATIONS as readonly string[]));
|
|
87
|
+
if (unique.size === 0) {
|
|
88
|
+
throw new Error("Rollup tiers must include at least one aggregation");
|
|
89
|
+
}
|
|
90
|
+
return Array.from(unique) as TimeSeriesAggregation[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeTimeSeriesTierInputs(tiers: TimeSeriesTierInput[]) {
|
|
94
|
+
if (tiers.length === 0) {
|
|
95
|
+
throw new Error("A time-series policy rule must include at least one tier");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let rawTierCount = 0;
|
|
99
|
+
const normalized = tiers.map((tier, index) => {
|
|
100
|
+
const fromAgeMs = parseDurationInput(tier.fromAge, `tiers[${index}].fromAge`);
|
|
101
|
+
const toAgeMs =
|
|
102
|
+
tier.toAge === null ? null : parseDurationInput(tier.toAge, `tiers[${index}].toAge`);
|
|
103
|
+
|
|
104
|
+
if (toAgeMs !== null && toAgeMs <= fromAgeMs) {
|
|
105
|
+
throw new Error(`tiers[${index}] must have toAge greater than fromAge`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (tier.kind === "raw") {
|
|
109
|
+
rawTierCount += 1;
|
|
110
|
+
return {
|
|
111
|
+
kind: "raw" as const,
|
|
112
|
+
fromAgeMs,
|
|
113
|
+
toAgeMs,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const bucketMs = parseDurationInput(tier.bucket, `tiers[${index}].bucket`);
|
|
118
|
+
if (bucketMs <= 0 || bucketMs % (60 * 1000) !== 0) {
|
|
119
|
+
throw new Error(`tiers[${index}].bucket must be a positive whole-minute duration`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind: "rollup" as const,
|
|
124
|
+
fromAgeMs,
|
|
125
|
+
toAgeMs,
|
|
126
|
+
bucketMs,
|
|
127
|
+
aggregations: normalizeAggregations(tier.aggregations),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (normalized[0].fromAgeMs !== 0) {
|
|
132
|
+
throw new Error("The first tier must start at age 0");
|
|
133
|
+
}
|
|
134
|
+
if (rawTierCount > 1) {
|
|
135
|
+
throw new Error("Only one raw tier is supported in a single policy rule");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < normalized.length - 1; index += 1) {
|
|
139
|
+
const current = normalized[index];
|
|
140
|
+
const next = normalized[index + 1];
|
|
141
|
+
|
|
142
|
+
if (current.toAgeMs === null) {
|
|
143
|
+
throw new Error("Open-ended tiers must be the final tier");
|
|
144
|
+
}
|
|
145
|
+
if (next.fromAgeMs !== current.toAgeMs) {
|
|
146
|
+
throw new Error("Policy tiers must be contiguous without gaps or overlap");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function normalizeTimeSeriesPolicyRuleInputs(rules: TimeSeriesPolicyRuleInput[]) {
|
|
154
|
+
const seenScopes = new Set<string>();
|
|
155
|
+
|
|
156
|
+
return rules.map((rule, index) => {
|
|
157
|
+
const scopeKey = createTimeSeriesPolicyScopeKey(rule.provider, rule.seriesType);
|
|
158
|
+
if (seenScopes.has(scopeKey)) {
|
|
159
|
+
throw new Error(`Duplicate time-series policy rule scope "${scopeKey}" at index ${index}`);
|
|
160
|
+
}
|
|
161
|
+
seenScopes.add(scopeKey);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
provider: rule.provider,
|
|
165
|
+
seriesType: rule.seriesType,
|
|
166
|
+
tiers: normalizeTimeSeriesTierInputs(rule.tiers),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findTierForAge(tiers: NormalizedTimeSeriesTier[], ageMs: number) {
|
|
172
|
+
return (
|
|
173
|
+
tiers.find(
|
|
174
|
+
(tier) => ageMs >= tier.fromAgeMs && (tier.toAgeMs === null || ageMs < tier.toAgeMs),
|
|
175
|
+
) ?? null
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getRawTier(tiers: NormalizedTimeSeriesTier[]) {
|
|
180
|
+
return tiers.find((tier) => tier.kind === "raw") ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getRollupTiers(tiers: NormalizedTimeSeriesTier[]) {
|
|
184
|
+
return tiers.filter((tier) => tier.kind === "rollup");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function buildBuiltinFullTiers(): NormalizedTimeSeriesTier[] {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
kind: "raw",
|
|
191
|
+
fromAgeMs: 0,
|
|
192
|
+
toAgeMs: null,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function resolveScopedPolicyRule<
|
|
198
|
+
Rule extends {
|
|
199
|
+
provider?: string;
|
|
200
|
+
seriesType?: string;
|
|
201
|
+
},
|
|
202
|
+
>(rules: Rule[], provider: string, seriesType: string) {
|
|
203
|
+
return (
|
|
204
|
+
rules.find((rule) => rule.provider === provider && rule.seriesType === seriesType) ??
|
|
205
|
+
rules.find((rule) => rule.provider === undefined && rule.seriesType === seriesType) ??
|
|
206
|
+
rules.find((rule) => rule.provider === provider && rule.seriesType === undefined) ??
|
|
207
|
+
rules.find((rule) => rule.provider === undefined && rule.seriesType === undefined) ??
|
|
208
|
+
null
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function comparePolicyScopes(
|
|
213
|
+
a: { provider?: string; seriesType?: string },
|
|
214
|
+
b: { provider?: string; seriesType?: string },
|
|
215
|
+
) {
|
|
216
|
+
const weight = (item: { provider?: string; seriesType?: string }) => {
|
|
217
|
+
const scope = inferTimeSeriesPolicyScope(item.provider, item.seriesType);
|
|
218
|
+
switch (scope) {
|
|
219
|
+
case "global":
|
|
220
|
+
return 0;
|
|
221
|
+
case "provider":
|
|
222
|
+
return 1;
|
|
223
|
+
case "series":
|
|
224
|
+
return 2;
|
|
225
|
+
case "provider_series":
|
|
226
|
+
return 3;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
weight(a) - weight(b) ||
|
|
232
|
+
(a.provider ?? "").localeCompare(b.provider ?? "") ||
|
|
233
|
+
(a.seriesType ?? "").localeCompare(b.seriesType ?? "")
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getBucketStart(recordedAt: number, bucketMs: number) {
|
|
238
|
+
return Math.floor(recordedAt / bucketMs) * bucketMs;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getBucketEnd(bucketStart: number, bucketMs: number) {
|
|
242
|
+
return bucketStart + bucketMs - 1;
|
|
243
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WorkflowManager } from "@convex-dev/workflow";
|
|
2
|
+
import type { ComponentApi as WorkflowComponentApi } from "@convex-dev/workflow/_generated/component.js";
|
|
3
|
+
import { components } from "./_generated/api";
|
|
4
|
+
|
|
5
|
+
// In this package, generated child components are typed as AnyComponents,
|
|
6
|
+
// so we narrow the installed workflow child component explicitly here.
|
|
7
|
+
const workflowComponent = components.workflow as unknown as WorkflowComponentApi<"workflow">;
|
|
8
|
+
|
|
9
|
+
export const durableWorkflow = new WorkflowManager(workflowComponent, {
|
|
10
|
+
workpoolOptions: {
|
|
11
|
+
maxParallelism: 5,
|
|
12
|
+
retryActionsByDefault: true,
|
|
13
|
+
defaultRetryBehavior: {
|
|
14
|
+
maxAttempts: 4,
|
|
15
|
+
initialBackoffMs: 1_000,
|
|
16
|
+
base: 2,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import workflowTest from "@convex-dev/workflow/test";
|
|
4
|
+
import workpoolTest from "@convex-dev/workpool/test";
|
|
5
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
6
|
+
import type { TestConvex } from "convex-test";
|
|
7
|
+
import schema from "./component/schema.js";
|
|
8
|
+
|
|
9
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register the component with a test Convex instance.
|
|
13
|
+
* @param t - The test Convex instance, for example from `convexTest`.
|
|
14
|
+
* @param name - The component name, as registered in the host app.
|
|
15
|
+
*/
|
|
16
|
+
export function register(
|
|
17
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
18
|
+
name: string = "wearables",
|
|
19
|
+
) {
|
|
20
|
+
t.registerComponent("workflow", workflowTest.schema, workflowTest.modules);
|
|
21
|
+
workpoolTest.register(t, "workflow/workpool");
|
|
22
|
+
t.registerComponent(name, schema, modules);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default { register, schema, modules };
|