@nativesquare/soma 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/LICENSE +201 -0
- package/README.md +142 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +279 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +264 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +5 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/component/_generated/api.d.ts +62 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +1063 -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/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/private.d.ts +33 -0
- package/dist/component/private.d.ts.map +1 -0
- package/dist/component/private.js +45 -0
- package/dist/component/private.js.map +1 -0
- package/dist/component/public.d.ts +1107 -0
- package/dist/component/public.d.ts.map +1 -0
- package/dist/component/public.js +310 -0
- package/dist/component/public.js.map +1 -0
- package/dist/component/schema.d.ts +4419 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +106 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/validators/activity.d.ts +747 -0
- package/dist/component/validators/activity.d.ts.map +1 -0
- package/dist/component/validators/activity.js +146 -0
- package/dist/component/validators/activity.js.map +1 -0
- package/dist/component/validators/athlete.d.ts +18 -0
- package/dist/component/validators/athlete.d.ts.map +1 -0
- package/dist/component/validators/athlete.js +25 -0
- package/dist/component/validators/athlete.js.map +1 -0
- package/dist/component/validators/body.d.ts +634 -0
- package/dist/component/validators/body.d.ts.map +1 -0
- package/dist/component/validators/body.js +70 -0
- package/dist/component/validators/body.js.map +1 -0
- package/dist/component/validators/connection.d.ts +7 -0
- package/dist/component/validators/connection.d.ts.map +1 -0
- package/dist/component/validators/connection.js +16 -0
- package/dist/component/validators/connection.js.map +1 -0
- package/dist/component/validators/daily.d.ts +650 -0
- package/dist/component/validators/daily.d.ts.map +1 -0
- package/dist/component/validators/daily.js +119 -0
- package/dist/component/validators/daily.js.map +1 -0
- package/dist/component/validators/enums.d.ts +24 -0
- package/dist/component/validators/enums.d.ts.map +1 -0
- package/dist/component/validators/enums.js +69 -0
- package/dist/component/validators/enums.js.map +1 -0
- package/dist/component/validators/index.d.ts +13 -0
- package/dist/component/validators/index.d.ts.map +1 -0
- package/dist/component/validators/index.js +16 -0
- package/dist/component/validators/index.js.map +1 -0
- package/dist/component/validators/menstruation.d.ts +51 -0
- package/dist/component/validators/menstruation.d.ts.map +1 -0
- package/dist/component/validators/menstruation.js +32 -0
- package/dist/component/validators/menstruation.js.map +1 -0
- package/dist/component/validators/nutrition.d.ts +498 -0
- package/dist/component/validators/nutrition.d.ts.map +1 -0
- package/dist/component/validators/nutrition.js +31 -0
- package/dist/component/validators/nutrition.js.map +1 -0
- package/dist/component/validators/plannedWorkout.d.ts +277 -0
- package/dist/component/validators/plannedWorkout.d.ts.map +1 -0
- package/dist/component/validators/plannedWorkout.js +105 -0
- package/dist/component/validators/plannedWorkout.js.map +1 -0
- package/dist/component/validators/samples.d.ts +609 -0
- package/dist/component/validators/samples.d.ts.map +1 -0
- package/dist/component/validators/samples.js +336 -0
- package/dist/component/validators/samples.js.map +1 -0
- package/dist/component/validators/shared.d.ts +402 -0
- package/dist/component/validators/shared.d.ts.map +1 -0
- package/dist/component/validators/shared.js +146 -0
- package/dist/component/validators/shared.js.map +1 -0
- package/dist/component/validators/sleep.d.ts +438 -0
- package/dist/component/validators/sleep.d.ts.map +1 -0
- package/dist/component/validators/sleep.js +95 -0
- package/dist/component/validators/sleep.js.map +1 -0
- package/dist/healthkit/activity.d.ts +75 -0
- package/dist/healthkit/activity.d.ts.map +1 -0
- package/dist/healthkit/activity.js +93 -0
- package/dist/healthkit/activity.js.map +1 -0
- package/dist/healthkit/athlete.d.ts +26 -0
- package/dist/healthkit/athlete.d.ts.map +1 -0
- package/dist/healthkit/athlete.js +34 -0
- package/dist/healthkit/athlete.js.map +1 -0
- package/dist/healthkit/body.d.ts +102 -0
- package/dist/healthkit/body.d.ts.map +1 -0
- package/dist/healthkit/body.js +167 -0
- package/dist/healthkit/body.js.map +1 -0
- package/dist/healthkit/daily.d.ts +119 -0
- package/dist/healthkit/daily.d.ts.map +1 -0
- package/dist/healthkit/daily.js +160 -0
- package/dist/healthkit/daily.js.map +1 -0
- package/dist/healthkit/index.d.ts +21 -0
- package/dist/healthkit/index.d.ts.map +1 -0
- package/dist/healthkit/index.js +21 -0
- package/dist/healthkit/index.js.map +1 -0
- package/dist/healthkit/maps/activity-type.d.ts +6 -0
- package/dist/healthkit/maps/activity-type.d.ts.map +1 -0
- package/dist/healthkit/maps/activity-type.js +184 -0
- package/dist/healthkit/maps/activity-type.js.map +1 -0
- package/dist/healthkit/maps/menstruation-flow.d.ts +6 -0
- package/dist/healthkit/maps/menstruation-flow.d.ts.map +1 -0
- package/dist/healthkit/maps/menstruation-flow.js +21 -0
- package/dist/healthkit/maps/menstruation-flow.js.map +1 -0
- package/dist/healthkit/maps/sleep-level.d.ts +11 -0
- package/dist/healthkit/maps/sleep-level.d.ts.map +1 -0
- package/dist/healthkit/maps/sleep-level.js +32 -0
- package/dist/healthkit/maps/sleep-level.js.map +1 -0
- package/dist/healthkit/menstruation.d.ts +35 -0
- package/dist/healthkit/menstruation.d.ts.map +1 -0
- package/dist/healthkit/menstruation.js +37 -0
- package/dist/healthkit/menstruation.js.map +1 -0
- package/dist/healthkit/nutrition.d.ts +77 -0
- package/dist/healthkit/nutrition.d.ts.map +1 -0
- package/dist/healthkit/nutrition.js +135 -0
- package/dist/healthkit/nutrition.js.map +1 -0
- package/dist/healthkit/sleep.d.ts +60 -0
- package/dist/healthkit/sleep.d.ts.map +1 -0
- package/dist/healthkit/sleep.js +108 -0
- package/dist/healthkit/sleep.js.map +1 -0
- package/dist/healthkit/types.d.ts +94 -0
- package/dist/healthkit/types.d.ts.map +1 -0
- package/dist/healthkit/types.js +26 -0
- package/dist/healthkit/types.js.map +1 -0
- package/dist/healthkit/utils.d.ts +63 -0
- package/dist/healthkit/utils.d.ts.map +1 -0
- package/dist/healthkit/utils.js +93 -0
- package/dist/healthkit/utils.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +112 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +371 -0
- package/src/client/types.ts +18 -0
- package/src/component/_generated/api.ts +78 -0
- package/src/component/_generated/component.ts +1090 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/private.ts +50 -0
- package/src/component/public.ts +358 -0
- package/src/component/schema.ts +115 -0
- package/src/component/validators/activity.ts +216 -0
- package/src/component/validators/athlete.ts +25 -0
- package/src/component/validators/body.ts +114 -0
- package/src/component/validators/connection.ts +16 -0
- package/src/component/validators/daily.ts +173 -0
- package/src/component/validators/enums.ts +119 -0
- package/src/component/validators/index.ts +16 -0
- package/src/component/validators/menstruation.ts +36 -0
- package/src/component/validators/nutrition.ts +37 -0
- package/src/component/validators/plannedWorkout.ts +110 -0
- package/src/component/validators/samples.ts +380 -0
- package/src/component/validators/shared.ts +165 -0
- package/src/component/validators/sleep.ts +133 -0
- package/src/healthkit/activity.ts +120 -0
- package/src/healthkit/athlete.ts +43 -0
- package/src/healthkit/body.ts +266 -0
- package/src/healthkit/daily.ts +245 -0
- package/src/healthkit/index.ts +59 -0
- package/src/healthkit/maps/activity-type.ts +185 -0
- package/src/healthkit/maps/menstruation-flow.ts +23 -0
- package/src/healthkit/maps/sleep-level.ts +37 -0
- package/src/healthkit/menstruation.ts +52 -0
- package/src/healthkit/nutrition.ts +162 -0
- package/src/healthkit/sleep.ts +136 -0
- package/src/healthkit/types.ts +219 -0
- package/src/healthkit/utils.ts +122 -0
- package/src/react/index.ts +7 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ─── Shared Utilities ────────────────────────────────────────────────────────
|
|
2
|
+
// Pure helper functions used across HealthKit transformer modules.
|
|
3
|
+
/**
|
|
4
|
+
* Compute the difference in seconds between two ISO-8601 timestamps.
|
|
5
|
+
*/
|
|
6
|
+
export function diffSeconds(start, end) {
|
|
7
|
+
return (new Date(end).getTime() - new Date(start).getTime()) / 1000;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build the start-of-day and end-of-day ISO-8601 strings for a date.
|
|
11
|
+
*/
|
|
12
|
+
export function dayRange(date) {
|
|
13
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
14
|
+
const dateStr = `${date.year}-${pad(date.month)}-${pad(date.day)}`;
|
|
15
|
+
return {
|
|
16
|
+
start_time: `${dateStr}T00:00:00.000Z`,
|
|
17
|
+
end_time: `${dateStr}T23:59:59.999Z`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Find the earliest startDate and latest endDate in an array of samples.
|
|
22
|
+
* Returns ISO-8601 strings. Falls back to provided defaults if array is empty.
|
|
23
|
+
*/
|
|
24
|
+
export function sampleTimeRange(samples, fallback) {
|
|
25
|
+
if (samples.length === 0) {
|
|
26
|
+
return (fallback ?? {
|
|
27
|
+
start_time: new Date().toISOString(),
|
|
28
|
+
end_time: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
let minStart = samples[0].startDate;
|
|
32
|
+
let maxEnd = samples[0].endDate;
|
|
33
|
+
for (const s of samples) {
|
|
34
|
+
if (s.startDate < minStart)
|
|
35
|
+
minStart = s.startDate;
|
|
36
|
+
if (s.endDate > maxEnd)
|
|
37
|
+
maxEnd = s.endDate;
|
|
38
|
+
}
|
|
39
|
+
return { start_time: minStart, end_time: maxEnd };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a Soma DeviceData object from HealthKit source and device metadata.
|
|
43
|
+
*/
|
|
44
|
+
export function buildDeviceData(source, device) {
|
|
45
|
+
if (!source && !device)
|
|
46
|
+
return undefined;
|
|
47
|
+
return {
|
|
48
|
+
name: device?.name ?? source?.name,
|
|
49
|
+
manufacturer: device?.manufacturer,
|
|
50
|
+
hardware_version: device?.hardwareVersion,
|
|
51
|
+
software_version: device?.softwareVersion,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Filter an array of HKQuantitySamples by sampleType identifier.
|
|
56
|
+
*/
|
|
57
|
+
export function filterByType(samples, sampleType) {
|
|
58
|
+
return samples.filter((s) => s.sampleType === sampleType);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sum the values of quantity samples.
|
|
62
|
+
*/
|
|
63
|
+
export function sumValues(samples) {
|
|
64
|
+
return samples.reduce((acc, s) => acc + s.value, 0);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Average the values of quantity samples.
|
|
68
|
+
* Returns undefined if the array is empty.
|
|
69
|
+
*/
|
|
70
|
+
export function avgValue(samples) {
|
|
71
|
+
if (samples.length === 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
return sumValues(samples) / samples.length;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Min value in a set of quantity samples.
|
|
77
|
+
* Returns undefined if the array is empty.
|
|
78
|
+
*/
|
|
79
|
+
export function minValue(samples) {
|
|
80
|
+
if (samples.length === 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
return Math.min(...samples.map((s) => s.value));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Max value in a set of quantity samples.
|
|
86
|
+
* Returns undefined if the array is empty.
|
|
87
|
+
*/
|
|
88
|
+
export function maxValue(samples) {
|
|
89
|
+
if (samples.length === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
return Math.max(...samples.map((s) => s.value));
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/healthkit/utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,UAE1B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,6CAA6C;AAE7C,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,EAAE;IACjC,OAAO,EAAE,CAAC;AACZ,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nativesquare/soma",
|
|
3
|
+
"description": "A Convex component that normalizes health and fitness data from multiple wearable providers into a single, consistent schema.",
|
|
4
|
+
"repository": "github:NativeSquare/soma",
|
|
5
|
+
"homepage": "https://github.com/NativeSquare/soma#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/NativeSquare/soma/issues"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"convex",
|
|
13
|
+
"component"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "run-p -r dev:*",
|
|
18
|
+
"dev:backend": "convex dev --typecheck-components",
|
|
19
|
+
"dev:frontend": "cd example && vite --clearScreen false",
|
|
20
|
+
"dev:build": "chokidar \"tsconfig*.json\" \"src/**/*.ts\" -i \"**/*.test.ts\" -c \"npm run build:codegen\" --initial",
|
|
21
|
+
"predev": "path-exists .env.local dist || (npm run build && convex dev --once)",
|
|
22
|
+
"build": "tsc --project ./tsconfig.build.json",
|
|
23
|
+
"build:codegen": "npx convex codegen --component-dir ./src/component && npm run build",
|
|
24
|
+
"build:clean": "npx rimraf dist *.tsbuildinfo && npm run build:codegen",
|
|
25
|
+
"typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"all": "run-p -r dev:* test:watch",
|
|
28
|
+
"test": "vitest run --typecheck",
|
|
29
|
+
"test:watch": "vitest --typecheck --clearScreen false",
|
|
30
|
+
"test:debug": "vitest --inspect-brk --no-file-parallelism",
|
|
31
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text",
|
|
32
|
+
"preversion": "npm ci && npm run build:clean && run-p test lint typecheck",
|
|
33
|
+
"prepublishOnly": "npm whoami || npm login",
|
|
34
|
+
"alpha": "npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags",
|
|
35
|
+
"release": "npm version patch && npm publish && git push --follow-tags",
|
|
36
|
+
"version": "vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src"
|
|
41
|
+
],
|
|
42
|
+
"exports": {
|
|
43
|
+
"./package.json": "./package.json",
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/client/index.d.ts",
|
|
46
|
+
"default": "./dist/client/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./react": {
|
|
49
|
+
"types": "./dist/react/index.d.ts",
|
|
50
|
+
"default": "./dist/react/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./healthkit": {
|
|
53
|
+
"types": "./dist/healthkit/index.d.ts",
|
|
54
|
+
"default": "./dist/healthkit/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./test": "./src/test.ts",
|
|
57
|
+
"./_generated/component.js": {
|
|
58
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
59
|
+
},
|
|
60
|
+
"./_generated/component": {
|
|
61
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./convex.config.js": {
|
|
64
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
65
|
+
"default": "./dist/component/convex.config.js"
|
|
66
|
+
},
|
|
67
|
+
"./convex.config": {
|
|
68
|
+
"types": "./dist/component/convex.config.d.ts",
|
|
69
|
+
"default": "./dist/component/convex.config.js"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"convex": "^1.31.7",
|
|
74
|
+
"react": "^18.3.1 || ^19.0.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@convex-dev/eslint-plugin": "^1.1.1",
|
|
78
|
+
"@edge-runtime/vm": "^5.0.0",
|
|
79
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
80
|
+
"@eslint/js": "9.39.2",
|
|
81
|
+
"@types/node": "^24.10.11",
|
|
82
|
+
"@types/react": "^19.2.13",
|
|
83
|
+
"@types/react-dom": "^19.2.3",
|
|
84
|
+
"@vitejs/plugin-react": "^5.1.3",
|
|
85
|
+
"chokidar-cli": "3.0.0",
|
|
86
|
+
"convex": "1.31.7",
|
|
87
|
+
"convex-test": "0.0.41",
|
|
88
|
+
"eslint": "9.39.2",
|
|
89
|
+
"eslint-plugin-react": "^7.37.5",
|
|
90
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
91
|
+
"eslint-plugin-react-refresh": "^0.5.0",
|
|
92
|
+
"globals": "^17.3.0",
|
|
93
|
+
"npm-run-all2": "8.0.4",
|
|
94
|
+
"path-exists-cli": "2.0.0",
|
|
95
|
+
"pkg-pr-new": "^0.0.63",
|
|
96
|
+
"prettier": "3.8.1",
|
|
97
|
+
"react": "^19.2.4",
|
|
98
|
+
"react-dom": "^19.2.4",
|
|
99
|
+
"typescript": "5.9.3",
|
|
100
|
+
"typescript-eslint": "8.54.0",
|
|
101
|
+
"vite": "7.3.1",
|
|
102
|
+
"vitest": "4.0.18"
|
|
103
|
+
},
|
|
104
|
+
"types": "./dist/client/index.d.ts",
|
|
105
|
+
"module": "./dist/client/index.js",
|
|
106
|
+
"pnpm": {
|
|
107
|
+
"onlyBuiltDependencies": [
|
|
108
|
+
"esbuild",
|
|
109
|
+
"@tailwindcss/oxide"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// This is only here so convex-test can detect a _generated folder
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
2
|
+
import type { MutationCtx, QueryCtx } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type SomaComponent = ComponentApi;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client class for the @nativesquare/soma Convex component.
|
|
8
|
+
*
|
|
9
|
+
* Provides a type-safe interface for managing wearable provider connections
|
|
10
|
+
* and querying normalized health & fitness data.
|
|
11
|
+
*
|
|
12
|
+
* All capabilities are also accessible via direct component function calls:
|
|
13
|
+
* `ctx.runMutation(components.soma.public.connect, { userId, provider: "GARMIN" })`
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // In your Convex function file:
|
|
18
|
+
* import { Soma } from "@nativesquare/soma";
|
|
19
|
+
* import { components } from "./_generated/api";
|
|
20
|
+
*
|
|
21
|
+
* const soma = new Soma(components.soma);
|
|
22
|
+
*
|
|
23
|
+
* // Connect a user to a provider:
|
|
24
|
+
* const connectionId = await soma.connect(ctx, {
|
|
25
|
+
* userId: "user_123",
|
|
26
|
+
* provider: "GARMIN",
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // List all connections:
|
|
30
|
+
* const connections = await soma.listConnections(ctx, { userId: "user_123" });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class Soma {
|
|
34
|
+
constructor(public component: SomaComponent) { }
|
|
35
|
+
|
|
36
|
+
// ─── Connect / Disconnect ───────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Connect a user to a wearable provider.
|
|
40
|
+
*
|
|
41
|
+
* Creates the connection if it doesn't exist, or re-activates it if it was
|
|
42
|
+
* previously disconnected. Idempotent — calling twice is a no-op.
|
|
43
|
+
*
|
|
44
|
+
* Use this when the host app has completed the provider's auth flow and
|
|
45
|
+
* wants to register the connection in Soma.
|
|
46
|
+
*
|
|
47
|
+
* @param ctx - Mutation context from the host app
|
|
48
|
+
* @param args.userId - The host app's user identifier (Clerk ID, etc.)
|
|
49
|
+
* @param args.provider - The wearable provider name ("GARMIN", "FITBIT", "OURA", etc.)
|
|
50
|
+
* @returns The connection document ID
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // "Connect to Garmin" button handler
|
|
55
|
+
* const connectionId = await soma.connect(ctx, {
|
|
56
|
+
* userId: "user_123",
|
|
57
|
+
* provider: "GARMIN",
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async connect(
|
|
62
|
+
ctx: MutationCtx,
|
|
63
|
+
args: { userId: string; provider: string },
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
return await ctx.runMutation(this.component.public.connect, args);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Disconnect a user from a wearable provider.
|
|
70
|
+
*
|
|
71
|
+
* Sets the connection to inactive. Does not delete any synced data,
|
|
72
|
+
* so re-connecting later preserves historical records.
|
|
73
|
+
*
|
|
74
|
+
* @param ctx - Mutation context from the host app
|
|
75
|
+
* @param args.userId - The host app's user identifier
|
|
76
|
+
* @param args.provider - The wearable provider name
|
|
77
|
+
*
|
|
78
|
+
* @throws Error if no connection exists for the given user–provider pair
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* // "Disconnect Garmin" button handler
|
|
83
|
+
* await soma.disconnect(ctx, {
|
|
84
|
+
* userId: "user_123",
|
|
85
|
+
* provider: "GARMIN",
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
async disconnect(
|
|
90
|
+
ctx: MutationCtx,
|
|
91
|
+
args: { userId: string; provider: string },
|
|
92
|
+
): Promise<null> {
|
|
93
|
+
return await ctx.runMutation(this.component.public.disconnect, args);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Connection Queries ─────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get a connection by its document ID.
|
|
100
|
+
*
|
|
101
|
+
* @param ctx - Query context from the host app
|
|
102
|
+
* @param args.connectionId - The connection document ID
|
|
103
|
+
* @returns The connection document, or null if not found
|
|
104
|
+
*/
|
|
105
|
+
async getConnection(
|
|
106
|
+
ctx: QueryCtx,
|
|
107
|
+
args: { connectionId: string },
|
|
108
|
+
) {
|
|
109
|
+
return await ctx.runQuery(this.component.public.getConnection, args);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the connection for a specific user–provider pair.
|
|
114
|
+
*
|
|
115
|
+
* Useful for checking whether a user is connected to a specific provider
|
|
116
|
+
* (e.g., rendering a "Connected" badge on a provider card).
|
|
117
|
+
*
|
|
118
|
+
* @param ctx - Query context from the host app
|
|
119
|
+
* @param args.userId - The host app's user identifier
|
|
120
|
+
* @param args.provider - The wearable provider name
|
|
121
|
+
* @returns The connection document, or null if never connected
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const garmin = await soma.getConnectionByProvider(ctx, {
|
|
126
|
+
* userId: "user_123",
|
|
127
|
+
* provider: "GARMIN",
|
|
128
|
+
* });
|
|
129
|
+
* const isConnected = garmin?.active === true;
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
async getConnectionByProvider(
|
|
133
|
+
ctx: QueryCtx,
|
|
134
|
+
args: { userId: string; provider: string },
|
|
135
|
+
) {
|
|
136
|
+
return await ctx.runQuery(
|
|
137
|
+
this.component.public.getConnectionByProvider,
|
|
138
|
+
args,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* List all connections for a user (active and inactive).
|
|
144
|
+
*
|
|
145
|
+
* @param ctx - Query context from the host app
|
|
146
|
+
* @param args.userId - The host app's user identifier
|
|
147
|
+
* @returns Array of connection documents
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const connections = await soma.listConnections(ctx, { userId: "user_123" });
|
|
152
|
+
* const activeProviders = connections
|
|
153
|
+
* .filter(c => c.active)
|
|
154
|
+
* .map(c => c.provider);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
async listConnections(
|
|
158
|
+
ctx: QueryCtx,
|
|
159
|
+
args: { userId: string },
|
|
160
|
+
) {
|
|
161
|
+
return await ctx.runQuery(this.component.public.listConnections, args);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Connection Mutations ───────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update a connection's mutable fields.
|
|
168
|
+
*
|
|
169
|
+
* @param ctx - Mutation context from the host app
|
|
170
|
+
* @param args.connectionId - The connection document ID
|
|
171
|
+
* @param args.active - Optional new active status
|
|
172
|
+
* @param args.lastDataUpdate - Optional ISO-8601 timestamp of last data sync
|
|
173
|
+
*
|
|
174
|
+
* @throws Error if the connection does not exist
|
|
175
|
+
*/
|
|
176
|
+
async updateConnection(
|
|
177
|
+
ctx: MutationCtx,
|
|
178
|
+
args: {
|
|
179
|
+
connectionId: string;
|
|
180
|
+
active?: boolean;
|
|
181
|
+
lastDataUpdate?: string;
|
|
182
|
+
},
|
|
183
|
+
): Promise<null> {
|
|
184
|
+
return await ctx.runMutation(
|
|
185
|
+
this.component.public.updateConnection,
|
|
186
|
+
args,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete a connection record entirely.
|
|
192
|
+
*
|
|
193
|
+
* This is a hard delete — the connection row is removed.
|
|
194
|
+
* Synced health data linked to this connection is NOT cascade-deleted.
|
|
195
|
+
*
|
|
196
|
+
* @param ctx - Mutation context from the host app
|
|
197
|
+
* @param args.connectionId - The connection document ID
|
|
198
|
+
*
|
|
199
|
+
* @throws Error if the connection does not exist
|
|
200
|
+
*/
|
|
201
|
+
async deleteConnection(
|
|
202
|
+
ctx: MutationCtx,
|
|
203
|
+
args: { connectionId: string },
|
|
204
|
+
): Promise<null> {
|
|
205
|
+
return await ctx.runMutation(
|
|
206
|
+
this.component.public.deleteConnection,
|
|
207
|
+
args,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Data Ingestion ─────────────────────────────────────────────────────────
|
|
212
|
+
// Store normalized health data into Soma with automatic deduplication.
|
|
213
|
+
// Use with transformer functions from @nativesquare/soma/healthkit:
|
|
214
|
+
//
|
|
215
|
+
// import { transformWorkout } from "@nativesquare/soma/healthkit";
|
|
216
|
+
// const data = transformWorkout(hkWorkout);
|
|
217
|
+
// await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Ingest an activity (workout) record.
|
|
221
|
+
*
|
|
222
|
+
* Upserts by `connectionId + metadata.summary_id` — re-ingesting the same
|
|
223
|
+
* workout updates the existing record rather than creating a duplicate.
|
|
224
|
+
*
|
|
225
|
+
* @param ctx - Mutation context from the host app
|
|
226
|
+
* @param args - Activity data including connectionId, userId, metadata, and all activity fields
|
|
227
|
+
* @returns The activity document ID
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* import { transformWorkout } from "@nativesquare/soma/healthkit";
|
|
232
|
+
* const data = transformWorkout(hkWorkout);
|
|
233
|
+
* const id = await soma.ingestActivity(ctx, { connectionId, userId, ...data });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
async ingestActivity(
|
|
237
|
+
ctx: MutationCtx,
|
|
238
|
+
args: IngestArgs,
|
|
239
|
+
): Promise<string> {
|
|
240
|
+
return await ctx.runMutation(
|
|
241
|
+
this.component.public.ingestActivity,
|
|
242
|
+
args as never,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Ingest a sleep session record.
|
|
248
|
+
*
|
|
249
|
+
* Upserts by `connectionId + metadata.summary_id`.
|
|
250
|
+
*
|
|
251
|
+
* @param ctx - Mutation context from the host app
|
|
252
|
+
* @param args - Sleep data including connectionId, userId, metadata, and all sleep fields
|
|
253
|
+
* @returns The sleep document ID
|
|
254
|
+
*/
|
|
255
|
+
async ingestSleep(
|
|
256
|
+
ctx: MutationCtx,
|
|
257
|
+
args: IngestArgs,
|
|
258
|
+
): Promise<string> {
|
|
259
|
+
return await ctx.runMutation(
|
|
260
|
+
this.component.public.ingestSleep,
|
|
261
|
+
args as never,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Ingest a body metrics record.
|
|
267
|
+
*
|
|
268
|
+
* Upserts by `connectionId + metadata.start_time + metadata.end_time`.
|
|
269
|
+
*
|
|
270
|
+
* @param ctx - Mutation context from the host app
|
|
271
|
+
* @param args - Body data including connectionId, userId, metadata, and all body fields
|
|
272
|
+
* @returns The body document ID
|
|
273
|
+
*/
|
|
274
|
+
async ingestBody(
|
|
275
|
+
ctx: MutationCtx,
|
|
276
|
+
args: IngestArgs,
|
|
277
|
+
): Promise<string> {
|
|
278
|
+
return await ctx.runMutation(
|
|
279
|
+
this.component.public.ingestBody,
|
|
280
|
+
args as never,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Ingest a daily activity summary record.
|
|
286
|
+
*
|
|
287
|
+
* Upserts by `connectionId + metadata.start_time + metadata.end_time`.
|
|
288
|
+
*
|
|
289
|
+
* @param ctx - Mutation context from the host app
|
|
290
|
+
* @param args - Daily data including connectionId, userId, metadata, and all daily fields
|
|
291
|
+
* @returns The daily document ID
|
|
292
|
+
*/
|
|
293
|
+
async ingestDaily(
|
|
294
|
+
ctx: MutationCtx,
|
|
295
|
+
args: IngestArgs,
|
|
296
|
+
): Promise<string> {
|
|
297
|
+
return await ctx.runMutation(
|
|
298
|
+
this.component.public.ingestDaily,
|
|
299
|
+
args as never,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Ingest a nutrition record.
|
|
305
|
+
*
|
|
306
|
+
* Upserts by `connectionId + metadata.start_time + metadata.end_time`.
|
|
307
|
+
*
|
|
308
|
+
* @param ctx - Mutation context from the host app
|
|
309
|
+
* @param args - Nutrition data including connectionId, userId, metadata, and all nutrition fields
|
|
310
|
+
* @returns The nutrition document ID
|
|
311
|
+
*/
|
|
312
|
+
async ingestNutrition(
|
|
313
|
+
ctx: MutationCtx,
|
|
314
|
+
args: IngestArgs,
|
|
315
|
+
): Promise<string> {
|
|
316
|
+
return await ctx.runMutation(
|
|
317
|
+
this.component.public.ingestNutrition,
|
|
318
|
+
args as never,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Ingest a menstruation record.
|
|
324
|
+
*
|
|
325
|
+
* Append-only — each call creates a new document.
|
|
326
|
+
*
|
|
327
|
+
* @param ctx - Mutation context from the host app
|
|
328
|
+
* @param args - Menstruation data including connectionId, userId, metadata, and menstruation fields
|
|
329
|
+
* @returns The menstruation document ID
|
|
330
|
+
*/
|
|
331
|
+
async ingestMenstruation(
|
|
332
|
+
ctx: MutationCtx,
|
|
333
|
+
args: IngestArgs,
|
|
334
|
+
): Promise<string> {
|
|
335
|
+
return await ctx.runMutation(
|
|
336
|
+
this.component.public.ingestMenstruation,
|
|
337
|
+
args as never,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Ingest an athlete (user profile) record.
|
|
343
|
+
*
|
|
344
|
+
* Upserts by `connectionId` — one athlete record per connection.
|
|
345
|
+
*
|
|
346
|
+
* @param ctx - Mutation context from the host app
|
|
347
|
+
* @param args - Athlete data including connectionId, userId, and profile fields
|
|
348
|
+
* @returns The athlete document ID
|
|
349
|
+
*/
|
|
350
|
+
async ingestAthlete(
|
|
351
|
+
ctx: MutationCtx,
|
|
352
|
+
args: IngestArgs,
|
|
353
|
+
): Promise<string> {
|
|
354
|
+
return await ctx.runMutation(
|
|
355
|
+
this.component.public.ingestAthlete,
|
|
356
|
+
args as never,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Common args shape for all ingestion methods.
|
|
363
|
+
*
|
|
364
|
+
* Requires `connectionId` and `userId` at minimum — additional fields
|
|
365
|
+
* come from the transformer output (e.g., `metadata`, `calories_data`, etc.)
|
|
366
|
+
* and are validated server-side by Convex validators.
|
|
367
|
+
*/
|
|
368
|
+
type IngestArgs = {
|
|
369
|
+
connectionId: string;
|
|
370
|
+
userId: string;
|
|
371
|
+
} & Record<string, unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GenericActionCtx,
|
|
3
|
+
GenericMutationCtx,
|
|
4
|
+
GenericQueryCtx,
|
|
5
|
+
GenericDataModel,
|
|
6
|
+
} from "convex/server";
|
|
7
|
+
|
|
8
|
+
export type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
9
|
+
|
|
10
|
+
export type MutationCtx = Pick<
|
|
11
|
+
GenericMutationCtx<GenericDataModel>,
|
|
12
|
+
"runQuery" | "runMutation"
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
export type ActionCtx = Pick<
|
|
16
|
+
GenericActionCtx<GenericDataModel>,
|
|
17
|
+
"runQuery" | "runMutation" | "runAction"
|
|
18
|
+
>;
|