diff --git a/.changeset/typed-healthkit-bindings.md b/.changeset/typed-healthkit-bindings.md new file mode 100644 index 00000000..5a1f87aa --- /dev/null +++ b/.changeset/typed-healthkit-bindings.md @@ -0,0 +1,21 @@ +--- +'@kingstinct/react-native-healthkit': major +--- + +Make typed `metadata` the canonical metadata API and generate more of the HealthKit type surface from Apple’s SDK. + +This release introduces generated identifier/value/unit metadata, typed metadata payloads on returned models, generic quantity sample typing, and SDK-backed schema verification to keep the surfaced API aligned with the pinned Xcode HealthKit SDK. + +Breaking changes: + +- Remove the legacy flattened `metadataX` fields from returned models. +- Make `metadata` the single canonical metadata surface. + +Migration examples: + +- `sample.metadataExternalUUID` -> `sample.metadata.HKExternalUUID` +- `sample.metadataWeatherCondition` -> `sample.metadata.HKWeatherCondition` +- `workout.metadataAverageMETs` -> `workout.metadata.HKAverageMETs` +- `categorySample.metadataMenstrualCycleStart` -> `categorySample.metadata.HKMenstrualCycleStart` + +This is intended to make the library easier to extend over time: Apple SDK metadata flows into the generated schema and typed `metadata` surface with much less hand-maintained code. diff --git a/.github/workflows/package-preview.yml b/.github/workflows/package-preview.yml index 6c5d17f0..6690bbc7 100644 --- a/.github/workflows/package-preview.yml +++ b/.github/workflows/package-preview.yml @@ -1,15 +1,15 @@ name: Package Preview on: - pull_request_target: + pull_request: + +permissions: {} jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: refs/pull/${{ github.event.number }}/merge - uses: oven-sh/setup-bun@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64866117..c85f9646 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,47 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: + changes: + runs-on: ubuntu-latest + outputs: + healthkit_contracts: ${{ steps.filter.outputs.healthkit_contracts }} + swift_native: ${{ steps.filter.outputs.swift_native }} + permissions: + pull-requests: read + + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + healthkit_contracts: + - '.bun-version' + - 'bun.lock' + - 'package.json' + - 'packages/react-native-healthkit/src/**' + - 'packages/react-native-healthkit/ios/**' + - 'packages/react-native-healthkit/cpp/**' + - 'packages/react-native-healthkit/scripts/**' + - 'packages/react-native-healthkit/nitrogen/**' + - 'packages/react-native-healthkit/package.json' + - 'packages/react-native-healthkit/tsconfig*.json' + - 'apps/example/contracts/**' + - 'apps/example/scripts/run-healthkit-contracts.sh' + - 'apps/example/app.json' + - 'apps/example/app/contracts.tsx' + - 'apps/example/app/_layout.tsx' + - 'apps/example/app/auth.tsx' + - 'apps/example/constants/AllUsedIdentifiersInApp.ts' + - 'apps/example/ios/**' + - 'apps/example/package.json' + - '.github/workflows/test.yml' + swift_native: + - 'packages/react-native-healthkit/ios/**' + - 'packages/react-native-healthkit/cpp/**' + - 'apps/example/ios/**' + test: # The type of runner that the job will run on runs-on: ${{ inputs.os || 'ubuntu-latest' }} @@ -95,8 +136,10 @@ jobs: run: bun run lint swiftlint: + needs: changes + if: needs.changes.outputs.swift_native == 'true' # The type of runner that the job will run on - runs-on: ${{ inputs.os || 'macos-latest' }} + runs-on: macos-15 timeout-minutes: 10 # Steps represent a sequence of tasks that will be executed as part of the job @@ -139,9 +182,11 @@ jobs: # xcodebuild -downloadPlatform iOS # # optional but useful: build-ios: + needs: changes + if: needs.changes.outputs.healthkit_contracts == 'true' # Only run on macOS since we need Xcode runs-on: macos-15 - timeout-minutes: 50 + timeout-minutes: 25 steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -157,13 +202,45 @@ jobs: ## Xcode 26.2 (Swift 6.2) - latest stable - run: sudo xcode-select -s /Applications/Xcode_26.2.app + - name: Install simulator tooling + run: | + brew tap wix/brew + brew install applesimutils + + - name: Verify generated HealthKit schema and bindings + working-directory: packages/react-native-healthkit + run: bun run check:generated + - run: bun run codegen working-directory: packages/react-native-healthkit - name: Expo Prebuild working-directory: apps/example - run: bunx expo prebuild --platform ios + run: bunx expo prebuild --platform ios --non-interactive - name: Build iOS project - working-directory: apps/example - run: bun run build-sim + working-directory: apps/example/ios + run: | + SIMULATOR_ID="$( + xcrun simctl list devices available | + sed -n 's/^[[:space:]]*iPhone[^()]* (\([0-9A-F-][0-9A-F-]*\)) (.*/\1/p' | + head -n 1 + )" + if [ -z "$SIMULATOR_ID" ]; then + echo "Unable to find an available iPhone simulator." >&2 + exit 1 + fi + xcodebuild \ + -quiet \ + -workspace RNHealthKit.xcworkspace \ + -scheme RNHealthKit \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination "id=$SIMULATOR_ID" \ + ONLY_ACTIVE_ARCH=YES \ + COMPILER_INDEX_STORE_ENABLE=NO \ + DEBUG_INFORMATION_FORMAT=dwarf \ + build + + - name: Run HealthKit contracts + run: bun run test:contracts diff --git a/.gitignore b/.gitignore index f26b5327..6e8db7c1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ DerivedData *.ipa *.xcuserstate project.xcworkspace +**/.build +**/.swiftpm # Android/IJ # @@ -79,4 +81,4 @@ lib/ packages/react-native-healthkit/nitrogen/generated apps/example/android -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo diff --git a/apps/example/app/(tabs)/index.tsx b/apps/example/app/(tabs)/index.tsx index 4e229c50..4eacea94 100644 --- a/apps/example/app/(tabs)/index.tsx +++ b/apps/example/app/(tabs)/index.tsx @@ -17,6 +17,7 @@ import { FitzpatrickSkinType, WheelchairUse, } from '@kingstinct/react-native-healthkit/types/Characteristics' +import { router } from 'expo-router' import { useEffect, useMemo, useState } from 'react' import { ListItem, type ListItemProps } from '@/components/SwiftListItem' import { enumKeyLookup } from '@/utils/enumKeyLookup' @@ -175,6 +176,15 @@ const CoreTab = () => { /> ))} +
+ { + router.push('/contracts') + }} + /> +
) diff --git a/apps/example/app/_layout.tsx b/apps/example/app/_layout.tsx index 4b2a2871..46aa149a 100644 --- a/apps/example/app/_layout.tsx +++ b/apps/example/app/_layout.tsx @@ -4,11 +4,13 @@ import { ThemeProvider, } from '@react-navigation/native' import { useFonts } from 'expo-font' -import { Stack } from 'expo-router' +import { Stack, usePathname, useRouter } from 'expo-router' import { StatusBar } from 'expo-status-bar' +import { useEffect } from 'react' import 'react-native-reanimated' import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { readLaunchCommand } from '@/contracts/launchCommand' import { useColorScheme } from '@/hooks/useColorScheme' export const unstable_settings = { @@ -17,10 +19,37 @@ export const unstable_settings = { export default function RootLayout() { const colorScheme = useColorScheme() + const pathname = usePathname() + const router = useRouter() const [loaded] = useFonts({ SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), }) + useEffect(() => { + if (!loaded) { + return + } + + const launchCommand = readLaunchCommand() + if (!launchCommand) { + return + } + + if ( + launchCommand.route === 'contracts' && + pathname !== '/contracts' && + pathname !== '/auth' + ) { + router.replace({ + pathname: '/contracts', + params: { + autorun: launchCommand.autorun, + scenario: launchCommand.scenario, + }, + }) + } + }, [loaded, pathname, router]) + if (!loaded) { // Async font loading only occurs in development. return null @@ -32,6 +61,12 @@ export default function RootLayout() { + { try { const res = await requestAuthorization({ @@ -32,11 +38,23 @@ export default function AuthScreen() { initSubscriptions() alert(`response: ${res}`) + if (launchCommand?.route === 'contracts') { + clearLaunchCommand() + router.replace({ + pathname: '/contracts', + params: { + autorun: launchCommand.autorun, + scenario: launchCommand.scenario, + }, + }) + return + } + router.replace('/') } catch (error) { console.error('Error requesting authorization:', error) } - }, []) + }, [launchCommand]) const [status, setStatus] = useState(null) @@ -63,6 +81,29 @@ export default function AuthScreen() { updateStatus() }, []) + useEffect(() => { + if (status === AuthorizationRequestStatus.unnecessary) { + if (launchCommand?.route === 'contracts') { + clearLaunchCommand() + router.replace({ + pathname: '/contracts', + params: { + autorun: launchCommand.autorun, + scenario: launchCommand.scenario, + }, + }) + } + return + } + + if ( + status === AuthorizationRequestStatus.shouldRequest && + launchCommand?.route === 'contracts' + ) { + void requestAuth() + } + }, [launchCommand, requestAuth, status]) + return ( diff --git a/apps/example/app/contracts.tsx b/apps/example/app/contracts.tsx new file mode 100644 index 00000000..844d70f7 --- /dev/null +++ b/apps/example/app/contracts.tsx @@ -0,0 +1,313 @@ +import { + getRequestStatusForAuthorization, + isHealthDataAvailable, + requestAuthorization, +} from '@kingstinct/react-native-healthkit' +import { AuthorizationRequestStatus } from '@kingstinct/react-native-healthkit/types/Auth' +import { useLocalSearchParams } from 'expo-router' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' +import { + AllObjectTypesInApp, + AllSampleTypesInApp, +} from '@/constants/AllUsedIdentifiersInApp' +import { clearLaunchCommand } from '@/contracts/launchCommand' +import { writeContractReport } from '@/contracts/report' +import { + contractScenarios, + runAllContractScenarios, +} from '@/contracts/scenarios' +import { enumKeyLookup } from '@/utils/enumKeyLookup' + +const requestStatusLookup = enumKeyLookup(AuthorizationRequestStatus) + +type ScenarioState = { + readonly ok: boolean + readonly details: readonly string[] + readonly payload?: unknown +} | null + +function stringifyPayload(value: unknown) { + return JSON.stringify( + value, + (_key, currentValue) => + currentValue instanceof Date ? currentValue.toISOString() : currentValue, + 2, + ) +} + +export default function ContractsScreen() { + const params = useLocalSearchParams<{ + autorun?: string + scenario?: string + }>() + const [authStatus, setAuthStatus] = + useState(null) + const [results, setResults] = useState>({}) + const [isRunning, setIsRunning] = useState(false) + const [output, setOutput] = useState('') + const [hasAutoRunStarted, setHasAutoRunStarted] = useState(false) + const [hasAutoRequestStarted, setHasAutoRequestStarted] = useState(false) + const wantsAutoRun = + params.autorun === 'all' || typeof params.scenario === 'string' + + const refreshAuthStatus = useCallback(async () => { + const status = await getRequestStatusForAuthorization({ + toRead: AllObjectTypesInApp, + toShare: AllSampleTypesInApp, + }) + setAuthStatus(status) + }, []) + + useEffect(() => { + void refreshAuthStatus() + }, [refreshAuthStatus]) + + const requestAuth = useCallback(async () => { + await requestAuthorization({ + toRead: AllObjectTypesInApp, + toShare: AllSampleTypesInApp, + }) + await refreshAuthStatus() + }, [refreshAuthStatus]) + + const runScenario = useCallback(async (scenarioId: string) => { + const scenario = contractScenarios.find((item) => item.id === scenarioId) + if (!scenario) { + return + } + + setIsRunning(true) + try { + const result = await scenario.run() + setResults((current) => ({ + ...current, + [scenario.id]: { + ok: result.ok, + details: result.details, + payload: result.payload, + }, + })) + setOutput(stringifyPayload(result)) + writeContractReport(result) + } finally { + setIsRunning(false) + } + }, []) + + const runAll = useCallback(async () => { + setIsRunning(true) + try { + const allResults = await runAllContractScenarios() + setResults( + Object.fromEntries( + allResults.map((result) => [ + result.id, + { + ok: result.ok, + details: result.details, + payload: result.payload, + }, + ]), + ), + ) + setOutput(stringifyPayload(allResults)) + writeContractReport(allResults) + } finally { + setIsRunning(false) + } + }, []) + + useEffect(() => { + if (wantsAutoRun) { + clearLaunchCommand() + } + }, [wantsAutoRun]) + + useEffect(() => { + if (!wantsAutoRun) { + return + } + if (authStatus !== AuthorizationRequestStatus.shouldRequest) { + return + } + if (hasAutoRequestStarted) { + return + } + + setHasAutoRequestStarted(true) + void requestAuth() + }, [authStatus, hasAutoRequestStarted, requestAuth, wantsAutoRun]) + + useEffect(() => { + if (authStatus !== AuthorizationRequestStatus.unnecessary) { + return + } + if (hasAutoRunStarted) { + return + } + + if (params.autorun === 'all') { + setHasAutoRunStarted(true) + void runAll() + return + } + + if (typeof params.scenario === 'string') { + setHasAutoRunStarted(true) + void runScenario(params.scenario) + } + }, [ + authStatus, + hasAutoRunStarted, + params.autorun, + params.scenario, + runAll, + runScenario, + ]) + + const overallStatus = useMemo(() => { + const values = Object.values(results) + if (values.length === 0) { + return 'Idle' + } + return values.every((value) => value?.ok) ? 'PASS' : 'FAIL' + }, [results]) + + return ( + + HealthKit Contracts + + Runtime smoke validation for the public JS API using the example app. + + + + Environment + + Health data available: {isHealthDataAvailable() ? 'YES' : 'NO'} + + + Auth status:{' '} + {authStatus == null ? 'LOADING' : requestStatusLookup[authStatus]} + + { + void requestAuth() + }} + style={styles.button} + testID="contracts.requestAuth" + > + Request Authorization + + + + + Run + { + void runAll() + }} + style={[styles.button, isRunning && styles.disabledButton]} + disabled={isRunning} + testID="contracts.runAll" + > + + {isRunning ? 'Running…' : 'Run All'} + + + Overall: {overallStatus} + + + {contractScenarios.map((scenario) => { + const result = results[scenario.id] + + return ( + + {scenario.title} + + Status: {result == null ? 'Idle' : result.ok ? 'PASS' : 'FAIL'} + + {result?.details?.length ? ( + {result.details.join('\n')} + ) : null} + { + void runScenario(scenario.id) + }} + style={[styles.button, isRunning && styles.disabledButton]} + disabled={isRunning} + testID={`contracts.scenario.${scenario.id}.run`} + > + Run + + + ) + })} + + + Output + + {output || 'No output yet.'} + + + + ) +} + +const styles = StyleSheet.create({ + container: { + gap: 16, + padding: 16, + paddingBottom: 48, + }, + title: { + fontSize: 28, + fontWeight: '700', + }, + subtitle: { + color: '#666', + lineHeight: 20, + }, + card: { + gap: 8, + padding: 16, + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 12, + backgroundColor: '#fff', + }, + cardTitle: { + fontSize: 18, + fontWeight: '600', + }, + button: { + alignSelf: 'flex-start', + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 999, + backgroundColor: '#111', + }, + disabledButton: { + opacity: 0.5, + }, + buttonLabel: { + color: '#fff', + fontWeight: '600', + }, + details: { + color: '#444', + lineHeight: 20, + }, + output: { + fontFamily: 'Menlo', + fontSize: 12, + lineHeight: 18, + }, +}) diff --git a/apps/example/constants/AllUsedIdentifiersInApp.ts b/apps/example/constants/AllUsedIdentifiersInApp.ts index 52ddc1b0..26fc8c4c 100644 --- a/apps/example/constants/AllUsedIdentifiersInApp.ts +++ b/apps/example/constants/AllUsedIdentifiersInApp.ts @@ -71,6 +71,7 @@ export const AllQuantityTypeIdentifierInApp: QuantityTypeIdentifierWriteable[] = export const AllCategorySampleTypeIdentifierInApp: CategoryTypeIdentifierWriteable[] = [ + 'HKCategoryTypeIdentifierMenstrualFlow', 'HKCategoryTypeIdentifierSleepAnalysis', 'HKCategoryTypeIdentifierSleepChanges', 'HKCategoryTypeIdentifierMindfulSession', diff --git a/apps/example/contracts/generated/healthkit.contract.generated.ts b/apps/example/contracts/generated/healthkit.contract.generated.ts new file mode 100644 index 00000000..b71e9bf4 --- /dev/null +++ b/apps/example/contracts/generated/healthkit.contract.generated.ts @@ -0,0 +1,361 @@ +/* + * AUTO-GENERATED FILE. DO NOT EDIT. + * Source: scripts/generate-healthkit.ts + */ + +import type { CategoryTypeIdentifier } from '@kingstinct/react-native-healthkit/types/CategoryTypeIdentifier' +import type { QuantityTypeIdentifier } from '@kingstinct/react-native-healthkit/types/QuantityTypeIdentifier' +import { z } from 'zod' + +export type ContractMetadataValueKind = + | 'string' + | 'boolean' + | 'number' + | 'quantity' + | 'enum' + +export const KNOWN_OBJECT_METADATA_KIND_BY_KEY = { + HKDeviceManufacturerName: 'string', + HKDeviceName: 'string', + HKDeviceSerialNumber: 'string', + HKDigitalSignature: 'string', + HKExternalUUID: 'string', + HKFoodType: 'string', + HKReferenceRangeLowerLimit: 'number', + HKReferenceRangeUpperLimit: 'number', + HKSyncIdentifier: 'string', + HKSyncVersion: 'number', + HKTimeZone: 'string', + HKUDIDeviceIdentifier: 'string', + HKUDIProductionIdentifier: 'string', + HKWasTakenInLab: 'boolean', + HKWasUserEntered: 'boolean', +} as const + +export const KNOWN_SAMPLE_METADATA_KIND_BY_KEY = { + HKActivityType: 'enum', + HKAlgorithmVersion: 'number', + HKAppleDeviceCalibrated: 'boolean', + HKAudioExposureDuration: 'quantity', + HKBarometricPressure: 'quantity', + HKDateOfEarliestDataUsedForEstimate: 'string', + HKMaximumLightIntensity: 'quantity', + HKPhysicalEffortEstimationType: 'enum', + HKUserMotionContext: 'enum', + HKWaterSalinity: 'enum', + HKWeatherCondition: 'enum', + HKWeatherHumidity: 'quantity', + HKWeatherTemperature: 'quantity', +} as const + +export const KNOWN_WORKOUT_METADATA_KIND_BY_KEY = { + HKAlpineSlopeGrade: 'quantity', + HKAppleFitnessPlusCatalogIdentifier: 'string', + HKAppleFitnessPlusSession: 'boolean', + HKAverageMETs: 'quantity', + HKAverageSpeed: 'quantity', + HKCoachedWorkout: 'boolean', + HKCrossTrainerDistance: 'quantity', + HKElevationAscended: 'quantity', + HKElevationDescended: 'quantity', + HKFitnessMachineDuration: 'quantity', + HKGroupFitness: 'boolean', + HKIndoorBikeDistance: 'quantity', + HKIndoorWorkout: 'boolean', + HKLapLength: 'quantity', + HKMaximumSpeed: 'quantity', + HKSwimmingLocationType: 'enum', + HKSWOLFScore: 'number', + HKWeatherCondition: 'enum', + HKWeatherHumidity: 'quantity', + HKWeatherTemperature: 'quantity', + HKWorkoutBrandName: 'string', +} as const + +export const KNOWN_WORKOUT_EVENT_METADATA_KIND_BY_KEY = { + HKSwimmingStrokeStyle: 'enum', +} as const + +export const KNOWN_CATEGORY_METADATA_KIND_BY_IDENTIFIER = { + HKCategoryTypeIdentifierAudioExposureEvent: { + HKAudioExposureLevel: 'quantity', + HKHeadphoneGain: 'quantity', + }, + HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent: { + HKAudioExposureLevel: 'quantity', + }, + HKCategoryTypeIdentifierHeadphoneAudioExposureEvent: { + HKAudioExposureLevel: 'quantity', + HKHeadphoneGain: 'quantity', + }, + HKCategoryTypeIdentifierHighHeartRateEvent: { + HKHeartRateEventThreshold: 'quantity', + }, + HKCategoryTypeIdentifierLowCardioFitnessEvent: { + HKLowCardioFitnessEventThreshold: 'quantity', + HKVO2MaxValue: 'quantity', + }, + HKCategoryTypeIdentifierLowHeartRateEvent: { + HKHeartRateEventThreshold: 'quantity', + }, + HKCategoryTypeIdentifierMenstrualFlow: { + HKMenstrualCycleStart: 'boolean', + }, + HKCategoryTypeIdentifierSexualActivity: { + HKSexualActivityProtectionUsed: 'boolean', + }, +} as const + +export const KNOWN_QUANTITY_METADATA_KIND_BY_IDENTIFIER = { + HKQuantityTypeIdentifierBasalBodyTemperature: { + HKBodyTemperatureSensorLocation: 'enum', + }, + HKQuantityTypeIdentifierBloodGlucose: { + HKBloodGlucoseMealTime: 'enum', + }, + HKQuantityTypeIdentifierBodyTemperature: { + HKBodyTemperatureSensorLocation: 'enum', + }, + HKQuantityTypeIdentifierInsulinDelivery: { + HKInsulinDeliveryReason: 'enum', + }, + HKQuantityTypeIdentifierVO2Max: { + HKVO2MaxTestType: 'enum', + }, +} as const + +const CATEGORY_METADATA_KIND_LOOKUP = + KNOWN_CATEGORY_METADATA_KIND_BY_IDENTIFIER as Readonly< + Record>> + > +const QUANTITY_METADATA_KIND_LOOKUP = + KNOWN_QUANTITY_METADATA_KIND_BY_IDENTIFIER as Readonly< + Record>> + > + +export const contractQuantitySchema = z + .object({ + unit: z.string(), + quantity: z.number(), + }) + .passthrough() + +export const contractSourceSchema = z + .object({ + name: z.string(), + bundleIdentifier: z.string(), + }) + .passthrough() + +export const contractSourceRevisionSchema = z + .object({ + source: contractSourceSchema, + operatingSystemVersion: z.string(), + version: z.string().optional(), + productType: z.string().optional(), + }) + .passthrough() + +export const contractDeviceSchema = z + .object({ + name: z.string().optional(), + firmwareVersion: z.string().optional(), + hardwareVersion: z.string().optional(), + localIdentifier: z.string().optional(), + manufacturer: z.string().optional(), + model: z.string().optional(), + softwareVersion: z.string().optional(), + udiDeviceIdentifier: z.string().optional(), + }) + .passthrough() + +export const contractSampleTypeSchema = z + .object({ + identifier: z.string(), + allowsRecalibrationForEstimates: z.boolean(), + isMinimumDurationRestricted: z.boolean(), + isMaximumDurationRestricted: z.boolean(), + }) + .passthrough() + +export const contractWorkoutActivitySchema = z + .object({ + startDate: z.date(), + endDate: z.date(), + uuid: z.string(), + duration: z.number(), + }) + .passthrough() + +function contractSchemaForMetadataKind( + kind: ContractMetadataValueKind, +): z.ZodTypeAny { + switch (kind) { + case 'string': + return z.string() + case 'boolean': + return z.boolean() + case 'number': + case 'enum': + return z.number() + case 'quantity': + return contractQuantitySchema + } +} + +function contractMetadataSchemaFromKinds( + kinds: Readonly>, +) { + const shape: Record = {} + + for (const [key, kind] of Object.entries(kinds)) { + shape[key] = contractSchemaForMetadataKind(kind).optional() + } + + return z.object(shape).passthrough() +} + +function mergeContractMetadataKinds( + ...sources: ReadonlyArray>> +): Record { + return Object.assign({}, ...sources) +} + +export const contractObjectMetadataSchema = contractMetadataSchemaFromKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, +) +export const contractSampleMetadataSchema = contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + ), +) +export const contractWorkoutMetadataSchema = contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + KNOWN_WORKOUT_METADATA_KIND_BY_KEY, + ), +) +export const contractWorkoutEventMetadataSchema = + contractMetadataSchemaFromKinds(KNOWN_WORKOUT_EVENT_METADATA_KIND_BY_KEY) + +const categoryMetadataSchemaLookup = Object.fromEntries( + Object.entries(CATEGORY_METADATA_KIND_LOOKUP).map(([identifier, kinds]) => [ + identifier, + contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + kinds, + ), + ), + ]), +) as Record + +const quantityMetadataSchemaLookup = Object.fromEntries( + Object.entries(QUANTITY_METADATA_KIND_LOOKUP).map(([identifier, kinds]) => [ + identifier, + contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + kinds, + ), + ), + ]), +) as Record + +export function getKnownCategoryMetadataKindMap( + identifier: CategoryTypeIdentifier, +): Readonly> { + return CATEGORY_METADATA_KIND_LOOKUP[identifier] ?? {} +} + +export function getKnownQuantityMetadataKindMap( + identifier: QuantityTypeIdentifier, +): Readonly> { + return QUANTITY_METADATA_KIND_LOOKUP[identifier] ?? {} +} + +export function getCategoryMetadataContractSchema( + identifier: CategoryTypeIdentifier, +): z.ZodTypeAny { + return ( + categoryMetadataSchemaLookup[identifier] ?? contractSampleMetadataSchema + ) +} + +export function getQuantityMetadataContractSchema( + identifier: QuantityTypeIdentifier, +): z.ZodTypeAny { + return ( + quantityMetadataSchemaLookup[identifier] ?? contractSampleMetadataSchema + ) +} + +function createBaseSampleContractSchema(metadataSchema: z.ZodTypeAny) { + return z + .object({ + uuid: z.string(), + sourceRevision: contractSourceRevisionSchema, + device: contractDeviceSchema.optional(), + metadata: metadataSchema, + sampleType: contractSampleTypeSchema, + startDate: z.date(), + endDate: z.date(), + hasUndeterminedDuration: z.boolean(), + }) + .passthrough() +} + +export function getQuantitySampleContractSchema( + identifier: QuantityTypeIdentifier, +) { + return createBaseSampleContractSchema( + getQuantityMetadataContractSchema(identifier), + ) + .extend({ + quantityType: z.literal(identifier), + quantity: z.number(), + unit: z.string(), + }) + .passthrough() +} + +export function getCategorySampleContractSchema( + identifier: CategoryTypeIdentifier, +) { + return createBaseSampleContractSchema( + getCategoryMetadataContractSchema(identifier), + ) + .extend({ + categoryType: z.literal(identifier), + value: z.number(), + }) + .passthrough() +} + +export const contractWorkoutEventSchema = z + .object({ + type: z.number(), + startDate: z.date(), + endDate: z.date(), + metadata: contractWorkoutEventMetadataSchema.optional(), + }) + .passthrough() + +export const contractWorkoutSampleSchema = createBaseSampleContractSchema( + contractWorkoutMetadataSchema, +) + .extend({ + workoutActivityType: z.number(), + duration: contractQuantitySchema, + totalEnergyBurned: contractQuantitySchema.optional(), + totalDistance: contractQuantitySchema.optional(), + totalSwimmingStrokeCount: contractQuantitySchema.optional(), + totalFlightsClimbed: contractQuantitySchema.optional(), + events: z.array(contractWorkoutEventSchema).optional(), + activities: z.array(contractWorkoutActivitySchema).optional(), + }) + .passthrough() diff --git a/apps/example/contracts/index.ts b/apps/example/contracts/index.ts new file mode 100644 index 00000000..6df73438 --- /dev/null +++ b/apps/example/contracts/index.ts @@ -0,0 +1,152 @@ +import type { CategorySampleTyped } from '@kingstinct/react-native-healthkit/types/CategoryType' +import type { CategoryTypeIdentifier } from '@kingstinct/react-native-healthkit/types/CategoryTypeIdentifier' +import type { QuantitySampleTyped } from '@kingstinct/react-native-healthkit/types/QuantitySample' +import type { QuantityTypeIdentifier } from '@kingstinct/react-native-healthkit/types/QuantityTypeIdentifier' +import type { + WorkoutEventTyped, + WorkoutSampleTyped, +} from '@kingstinct/react-native-healthkit/types/Workouts' +import type { z } from 'zod' +import { + contractWorkoutEventSchema, + contractWorkoutSampleSchema, + getCategorySampleContractSchema, + getQuantitySampleContractSchema, +} from './generated/healthkit.contract.generated' + +export { + contractWorkoutEventSchema, + contractWorkoutSampleSchema, + getCategorySampleContractSchema, + getQuantitySampleContractSchema, +} from './generated/healthkit.contract.generated' + +export interface ContractValidationIssue { + readonly path: string + readonly message: string +} + +export interface ContractValidationResult { + readonly ok: boolean + readonly issues: readonly ContractValidationIssue[] +} + +function formatPathSegment(segment: string | number): string { + if (typeof segment === 'number') { + return `[${segment}]` + } + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(segment) + ? `.${segment}` + : `[${JSON.stringify(segment)}]` +} + +function formatIssuePath( + rootPath: string, + path: readonly (string | number)[], +): string { + if (path.length === 0) { + return rootPath + } + + return `${rootPath}${path.map(formatPathSegment).join('')}` +} + +function issuesFromError( + rootPath: string, + error: z.ZodError, +): readonly ContractValidationIssue[] { + return error.issues.map((issue) => ({ + path: formatIssuePath(rootPath, issue.path as Array), + message: issue.message, + })) +} + +function safeParseContract( + rootPath: string, + schema: z.ZodTypeAny, + value: unknown, +): ContractValidationResult { + const result = schema.safeParse(value) + if (result.success) { + return { ok: true, issues: [] } + } + + return { + ok: false, + issues: issuesFromError(rootPath, result.error), + } +} + +function formatIssues(issues: readonly ContractValidationIssue[]): string { + return issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n') +} + +export function validateQuantitySampleContract< + T extends QuantityTypeIdentifier, +>(identifier: T, sample: unknown): ContractValidationResult { + return safeParseContract( + 'quantitySample', + getQuantitySampleContractSchema(identifier), + sample, + ) +} + +export function validateCategorySampleContract< + T extends CategoryTypeIdentifier, +>(identifier: T, sample: unknown): ContractValidationResult { + return safeParseContract( + 'categorySample', + getCategorySampleContractSchema(identifier), + sample, + ) +} + +export function validateWorkoutEventContract( + event: unknown, +): ContractValidationResult { + return safeParseContract('workoutEvent', contractWorkoutEventSchema, event) +} + +export function validateWorkoutSampleContract( + sample: unknown, +): ContractValidationResult { + return safeParseContract('workoutSample', contractWorkoutSampleSchema, sample) +} + +export function assertQuantitySampleContract( + identifier: T, + sample: unknown, +): asserts sample is QuantitySampleTyped { + const result = validateQuantitySampleContract(identifier, sample) + if (!result.ok) { + throw new Error(formatIssues(result.issues)) + } +} + +export function assertCategorySampleContract( + identifier: T, + sample: unknown, +): asserts sample is CategorySampleTyped { + const result = validateCategorySampleContract(identifier, sample) + if (!result.ok) { + throw new Error(formatIssues(result.issues)) + } +} + +export function assertWorkoutSampleContract( + sample: unknown, +): asserts sample is WorkoutSampleTyped { + const result = validateWorkoutSampleContract(sample) + if (!result.ok) { + throw new Error(formatIssues(result.issues)) + } +} + +export function assertWorkoutEventContract( + event: unknown, +): asserts event is WorkoutEventTyped { + const result = validateWorkoutEventContract(event) + if (!result.ok) { + throw new Error(formatIssues(result.issues)) + } +} diff --git a/apps/example/contracts/launchCommand.ts b/apps/example/contracts/launchCommand.ts new file mode 100644 index 00000000..596cbfb7 --- /dev/null +++ b/apps/example/contracts/launchCommand.ts @@ -0,0 +1,50 @@ +import { File, Paths } from 'expo-file-system' + +export interface LaunchCommand { + readonly route: 'contracts' + readonly autorun?: 'all' + readonly scenario?: string +} + +export const contractLaunchCommandFile = new File( + Paths.document, + 'healthkit-contract-command.json', +) + +export function readLaunchCommand(): LaunchCommand | null { + if (!contractLaunchCommandFile.exists) { + return null + } + + try { + const value = JSON.parse( + contractLaunchCommandFile.textSync(), + ) as LaunchCommand + + return value + } catch (error) { + console.error('Failed to read contract launch command:', error) + return null + } +} + +export function clearLaunchCommand() { + if (!contractLaunchCommandFile.exists) { + return + } + + try { + contractLaunchCommandFile.delete() + } catch (error) { + console.error('Failed to delete contract launch command:', error) + } +} + +export function readAndClearLaunchCommand(): LaunchCommand | null { + const command = readLaunchCommand() + if (!command) { + return null + } + clearLaunchCommand() + return command +} diff --git a/apps/example/contracts/report.ts b/apps/example/contracts/report.ts new file mode 100644 index 00000000..95403f93 --- /dev/null +++ b/apps/example/contracts/report.ts @@ -0,0 +1,23 @@ +import { File, Paths } from 'expo-file-system' + +export const contractReportFile = new File( + Paths.document, + 'healthkit-contract-report.json', +) + +export function writeContractReport(value: unknown) { + contractReportFile.create({ + overwrite: true, + intermediates: true, + }) + contractReportFile.write( + JSON.stringify( + value, + (_key, currentValue) => + currentValue instanceof Date + ? currentValue.toISOString() + : currentValue, + 2, + ), + ) +} diff --git a/apps/example/contracts/scenarios.ts b/apps/example/contracts/scenarios.ts new file mode 100644 index 00000000..f25b3590 --- /dev/null +++ b/apps/example/contracts/scenarios.ts @@ -0,0 +1,294 @@ +import { + CategoryValueMenstrualFlow, + deleteObjects, + queryCategorySamples, + queryQuantitySamples, + queryWorkoutSamples, + saveCategorySample, + saveQuantitySample, + saveWorkoutSample, + WorkoutActivityType, +} from '@kingstinct/react-native-healthkit' +import { BloodGlucoseMealTime } from '@kingstinct/react-native-healthkit/types/MetadataEnums' +import { + assertCategorySampleContract, + assertQuantitySampleContract, + assertWorkoutSampleContract, +} from '@/contracts' + +export type ContractScenarioId = + | 'quantity-roundtrip' + | 'category-roundtrip' + | 'workout-roundtrip' + +export interface ContractScenarioResult { + readonly id: ContractScenarioId + readonly title: string + readonly ok: boolean + readonly details: readonly string[] + readonly payload?: unknown +} + +export interface ContractScenario { + readonly id: ContractScenarioId + readonly title: string + readonly run: () => Promise +} + +function success( + id: ContractScenarioId, + title: string, + payload: unknown, + details: string[] = [], +): ContractScenarioResult { + return { + id, + title, + ok: true, + details, + payload, + } +} + +function failure( + id: ContractScenarioId, + title: string, + error: unknown, +): ContractScenarioResult { + return { + id, + title, + ok: false, + details: [error instanceof Error ? error.message : String(error)], + } +} + +async function cleanupSample(identifier: string, uuid: string) { + try { + await deleteObjects(identifier as never, { + uuids: [uuid], + }) + } catch (error) { + console.warn(`Failed to delete sample ${identifier}/${uuid}:`, error) + } +} + +const quantityScenario: ContractScenario = { + id: 'quantity-roundtrip', + title: 'Quantity Round-trip', + run: async () => { + const start = new Date(Date.now() - 60_000) + const end = new Date() + + try { + const saved = await saveQuantitySample( + 'HKQuantityTypeIdentifierBloodGlucose', + 'mg/dL', + 94, + start, + end, + { + HKBloodGlucoseMealTime: BloodGlucoseMealTime.preprandial, + HKWasUserEntered: true, + }, + ) + + if (!saved) { + throw new Error('saveQuantitySample returned undefined') + } + + assertQuantitySampleContract( + 'HKQuantityTypeIdentifierBloodGlucose', + saved, + ) + + const queried = await queryQuantitySamples( + 'HKQuantityTypeIdentifierBloodGlucose', + { + limit: 1, + unit: 'mg/dL', + filter: { + uuids: [saved.uuid], + }, + }, + ) + + const roundTripped = queried[0] + if (!roundTripped) { + throw new Error('queryQuantitySamples returned no matching sample') + } + + assertQuantitySampleContract( + 'HKQuantityTypeIdentifierBloodGlucose', + roundTripped, + ) + + if ( + roundTripped.metadata.HKBloodGlucoseMealTime !== + BloodGlucoseMealTime.preprandial + ) { + throw new Error('Expected HKBloodGlucoseMealTime to survive round-trip') + } + + await cleanupSample('HKQuantityTypeIdentifierBloodGlucose', saved.uuid) + + return success( + quantityScenario.id, + quantityScenario.title, + roundTripped, + ['saveQuantitySample', 'queryQuantitySamples'], + ) + } catch (error) { + return failure(quantityScenario.id, quantityScenario.title, error) + } + }, +} + +const categoryScenario: ContractScenario = { + id: 'category-roundtrip', + title: 'Category Round-trip', + run: async () => { + const start = new Date(Date.now() - 60_000) + const end = new Date() + + try { + const saved = await saveCategorySample( + 'HKCategoryTypeIdentifierMenstrualFlow', + CategoryValueMenstrualFlow.light, + start, + end, + { + HKMenstrualCycleStart: true, + HKWasUserEntered: true, + }, + ) + + if (!saved) { + throw new Error('saveCategorySample returned undefined') + } + + assertCategorySampleContract( + 'HKCategoryTypeIdentifierMenstrualFlow', + saved, + ) + + const queried = await queryCategorySamples( + 'HKCategoryTypeIdentifierMenstrualFlow', + { + limit: 1, + filter: { + uuids: [saved.uuid], + }, + }, + ) + + const roundTripped = queried[0] + if (!roundTripped) { + throw new Error('queryCategorySamples returned no matching sample') + } + + assertCategorySampleContract( + 'HKCategoryTypeIdentifierMenstrualFlow', + roundTripped, + ) + + if (roundTripped.metadata.HKMenstrualCycleStart !== true) { + throw new Error('Expected HKMenstrualCycleStart to survive round-trip') + } + + await cleanupSample('HKCategoryTypeIdentifierMenstrualFlow', saved.uuid) + + return success( + categoryScenario.id, + categoryScenario.title, + roundTripped, + ['saveCategorySample', 'queryCategorySamples'], + ) + } catch (error) { + return failure(categoryScenario.id, categoryScenario.title, error) + } + }, +} + +const workoutScenario: ContractScenario = { + id: 'workout-roundtrip', + title: 'Workout Round-trip', + run: async () => { + const start = new Date(Date.now() - 30 * 60_000) + const end = new Date() + + try { + const saved = await saveWorkoutSample( + WorkoutActivityType.running, + [ + { + quantityType: 'HKQuantityTypeIdentifierDistanceWalkingRunning', + quantity: 1250, + unit: 'm', + startDate: start, + endDate: end, + metadata: {}, + }, + ], + start, + end, + { + distance: 1250, + }, + { + HKWorkoutBrandName: 'contract-harness', + HKIndoorWorkout: true, + }, + ) + + const savedJson = saved.toJSON() + assertWorkoutSampleContract(savedJson) + + const queried = await queryWorkoutSamples({ + limit: 1, + filter: { + uuids: [saved.uuid], + }, + }) + + const roundTripped = queried[0] + if (!roundTripped) { + throw new Error('queryWorkoutSamples returned no matching workout') + } + + const roundTrippedJson = roundTripped.toJSON() + assertWorkoutSampleContract(roundTrippedJson) + + if (roundTrippedJson.metadata.HKWorkoutBrandName !== 'contract-harness') { + throw new Error('Expected HKWorkoutBrandName to survive round-trip') + } + + await cleanupSample('HKWorkoutTypeIdentifier', saved.uuid) + + return success( + workoutScenario.id, + workoutScenario.title, + roundTrippedJson, + ['saveWorkoutSample', 'queryWorkoutSamples'], + ) + } catch (error) { + return failure(workoutScenario.id, workoutScenario.title, error) + } + }, +} + +export const contractScenarios: readonly ContractScenario[] = [ + quantityScenario, + categoryScenario, + workoutScenario, +] + +export async function runAllContractScenarios() { + const results: ContractScenarioResult[] = [] + + for (const scenario of contractScenarios) { + results.push(await scenario.run()) + } + + return results +} diff --git a/apps/example/package.json b/apps/example/package.json index 1832154c..36a75c31 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -4,6 +4,7 @@ "version": "1.0.0", "scripts": { "start": "expo start", + "contracts": "sh ./scripts/run-healthkit-contracts.sh", "reset-project": "node ./scripts/reset-project.js", "android": "expo run:android", "ios": "expo run:ios", @@ -45,7 +46,8 @@ "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.16.0", "react-native-web": "^0.21.2", - "react-native-webview": "13.15.0" + "react-native-webview": "13.15.0", + "zod": "^4.2.1" }, "devDependencies": { "@babel/core": "^7.28.5", diff --git a/apps/example/scripts/run-healthkit-contracts.sh b/apps/example/scripts/run-healthkit-contracts.sh new file mode 100644 index 00000000..779cb179 --- /dev/null +++ b/apps/example/scripts/run-healthkit-contracts.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" +APP_DIR="$ROOT_DIR/apps/example" +SIMULATOR_ID="${SIMULATOR_ID:-${1:-}}" +APP_ID="com.kingstinct.reactnativehealthkitexample" +INITIAL_URL="exp+react-native-healthkit-example://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A8081" +METRO_LOG="${TMPDIR:-/tmp}/healthkit-contract-metro.log" +APP_DATA="" +REPORT_PATH="" +COMMAND_PATH="" +METRO_PID="" + +run_with_timeout() { + local seconds="$1" + shift + python3 - "$seconds" "$@" <<'PY' +import subprocess +import sys + +timeout = int(sys.argv[1]) +command = sys.argv[2:] + +try: + subprocess.run(command, check=True, timeout=timeout) +except subprocess.TimeoutExpired: + print( + f"Timed out after {timeout}s: {' '.join(command)}", + file=sys.stderr, + ) + sys.exit(124) +except subprocess.CalledProcessError as error: + sys.exit(error.returncode) +PY +} + +dump_debug_artifacts() { + local reason="$1" + + echo "$reason" >&2 + + if [ -n "$SIMULATOR_ID" ]; then + local screenshot_path + screenshot_path="$(mktemp -t healthkit-contracts-XXXXXX).png" + if xcrun simctl io "$SIMULATOR_ID" screenshot "$screenshot_path" >/dev/null 2>&1; then + echo "Simulator screenshot: $screenshot_path" >&2 + fi + fi + + if [ -n "$APP_DATA" ]; then + echo "App data container: $APP_DATA" >&2 + fi + + if [ -n "$REPORT_PATH" ] && [ -f "$REPORT_PATH" ]; then + echo "Partial contract report:" >&2 + cat "$REPORT_PATH" >&2 + fi + + if [ -f "$METRO_LOG" ]; then + echo "Metro log tail:" >&2 + tail -n 200 "$METRO_LOG" >&2 + fi +} + +cleanup() { + if [ -n "$METRO_PID" ] && kill -0 "$METRO_PID" 2>/dev/null; then + kill "$METRO_PID" 2>/dev/null || true + fi +} + +trap cleanup EXIT INT TERM +trap 'dump_debug_artifacts "Contract runner failed at line $LINENO"' ERR + +find_booted_simulator() { + xcrun simctl list devices | + sed -n 's/.*(\([0-9A-F-][0-9A-F-]*\)) (Booted).*/\1/p' | + head -n 1 +} + +find_available_simulator() { + xcrun simctl list devices available | + sed -n 's/^[[:space:]]*iPhone[^()]* (\([0-9A-F-][0-9A-F-]*\)) (.*/\1/p' | + head -n 1 +} + +if [ -z "$SIMULATOR_ID" ]; then + SIMULATOR_ID="$(find_booted_simulator)" +fi + +if [ -z "$SIMULATOR_ID" ]; then + SIMULATOR_ID="$(find_available_simulator)" +fi + +if [ -z "$SIMULATOR_ID" ]; then + echo "No usable simulator found. Set SIMULATOR_ID or install an iPhone simulator runtime." >&2 + exit 1 +fi + +if ! xcrun simctl list devices | grep -q "$SIMULATOR_ID.*Booted"; then + run_with_timeout 30 xcrun simctl boot "$SIMULATOR_ID" + run_with_timeout 120 xcrun simctl bootstatus "$SIMULATOR_ID" -b +fi + +if ! command -v applesimutils >/dev/null 2>&1; then + echo "applesimutils is required for HealthKit contract runs." >&2 + exit 1 +fi + +APP_BUNDLE="${APP_BUNDLE:-}" +if [ -z "$APP_BUNDLE" ]; then + APP_BUNDLE="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path '*Debug-iphonesimulator/RNHealthKit.app' | sort | tail -n 1)" +fi + +if [ ! -d "$APP_BUNDLE" ]; then + echo "Could not find built RNHealthKit.app. Set APP_BUNDLE or build the example app first." >&2 + exit 1 +fi + +rm -f "$METRO_LOG" + +if ! curl -fsS --max-time 2 "http://127.0.0.1:8081/status" >/dev/null 2>&1; then + ( + cd "$APP_DIR" + bun start --clear --non-interactive >"$METRO_LOG" 2>&1 + ) & + METRO_PID="$!" + + ATTEMPT=0 + until curl -fsS --max-time 2 "http://127.0.0.1:8081/status" >/dev/null 2>&1; do + ATTEMPT=$((ATTEMPT + 1)) + if [ "$ATTEMPT" -ge 60 ]; then + dump_debug_artifacts "Metro did not start in time." + exit 1 + fi + sleep 1 + done +fi + +run_with_timeout 20 xcrun simctl terminate "$SIMULATOR_ID" "$APP_ID" >/dev/null 2>&1 || true +run_with_timeout 20 xcrun simctl uninstall "$SIMULATOR_ID" "$APP_ID" >/dev/null 2>&1 || true +run_with_timeout 60 xcrun simctl install "$SIMULATOR_ID" "$APP_BUNDLE" + +APP_DATA="$(xcrun simctl get_app_container "$SIMULATOR_ID" "$APP_ID" data)" +mkdir -p "$APP_DATA/Documents" +REPORT_PATH="$APP_DATA/Documents/healthkit-contract-report.json" +COMMAND_PATH="$APP_DATA/Documents/healthkit-contract-command.json" +rm -f "$REPORT_PATH" +printf '%s\n' '{"route":"contracts","autorun":"all"}' >"$COMMAND_PATH" + +run_with_timeout 20 applesimutils \ + --byId "$SIMULATOR_ID" \ + --bundle "$APP_ID" \ + --setPermissions 'health=YES,motion=YES' + +run_with_timeout 30 xcrun simctl launch "$SIMULATOR_ID" "$APP_ID" --initialUrl "$INITIAL_URL" >/dev/null + +ATTEMPT=0 +until [ -f "$REPORT_PATH" ]; do + ATTEMPT=$((ATTEMPT + 1)) + if [ "$ATTEMPT" -ge 90 ]; then + dump_debug_artifacts "Contract report was not produced." + exit 1 + fi + sleep 1 +done + +cat "$REPORT_PATH" + +REPORT_PATH="$REPORT_PATH" bun -e ' + import { readFileSync } from "node:fs" + + const report = JSON.parse(readFileSync(process.env.REPORT_PATH, "utf8")) + const results = Array.isArray(report) ? report : [report] + const failures = results.filter((entry) => entry?.ok !== true) + if (failures.length > 0) { + console.error(`Contract failures: ${failures.map((entry) => entry.id).join(", ")}`) + process.exit(1) + } +' diff --git a/biome.json b/biome.json index fe5e02d7..5c3e6ef3 100644 --- a/biome.json +++ b/biome.json @@ -55,9 +55,12 @@ "files": { "includes": [ "**/*", + "!**/.build", + "!**/.swiftpm", "!**/docs", "!**/lib", "!**/.expo", + "!apps/example/expo-env.d.ts", "!**/ios", "!.vscode/*", "!.claude/*" diff --git a/bun.lock b/bun.lock index 11afc947..9fd3479b 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "react-native-healthkit-mono", @@ -55,6 +56,7 @@ "react-native-screens": "~4.16.0", "react-native-web": "^0.21.2", "react-native-webview": "13.15.0", + "zod": "^4.2.1", }, "devDependencies": { "@babel/core": "^7.28.5", @@ -65,7 +67,7 @@ }, "packages/react-native-healthkit": { "name": "@kingstinct/react-native-healthkit", - "version": "13.2.3", + "version": "13.4.0", "devDependencies": { "@testing-library/react-native": "^13.3.3", "@types/react": "~19.1.17", diff --git a/package.json b/package.json index 7729d52e..97f54f39 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "codegen": "cd packages/react-native-healthkit && rm -rf nitrogen/generated && bun run nitrogen && cd ../../ && bun run fix-codegen && cd apps/example/ios && pod install", "fix-codegen": "perl -i -pe 's/Bool\\(fromCxx: cachedCxxPart\\)/cachedCxxPart.use_count() > 0/g' packages/react-native-healthkit/nitrogen/generated/ios/swift/*Spec_cxx.swift", "test": "cd packages/react-native-healthkit && bun run test", + "test:contracts": "cd apps/example && bun run contracts", "prepare": "husky", "reinstall": "rm -rf node_modules/ && find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && bun install", "reinstall-with-nuked-lockfile": "rm -rf node_modules/ && rm bun.lock && find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && bun install" diff --git a/packages/react-native-healthkit/ios/CategoryTypeModule.swift b/packages/react-native-healthkit/ios/CategoryTypeModule.swift index dcb31e26..b3e2f8c4 100644 --- a/packages/react-native-healthkit/ios/CategoryTypeModule.swift +++ b/packages/react-native-healthkit/ios/CategoryTypeModule.swift @@ -3,7 +3,7 @@ import NitroModules class CategoryTypeModule: HybridCategoryTypeModuleSpec { func saveCategorySample( - identifier: CategoryTypeIdentifier, + identifier: CategoryTypeIdentifierWriteable, value: Double, startDate: Date, endDate: Date, diff --git a/packages/react-native-healthkit/ios/CorrelationTypeModule.swift b/packages/react-native-healthkit/ios/CorrelationTypeModule.swift index 323ec927..cf088707 100644 --- a/packages/react-native-healthkit/ios/CorrelationTypeModule.swift +++ b/packages/react-native-healthkit/ios/CorrelationTypeModule.swift @@ -24,47 +24,14 @@ func serializeCorrelationSample(correlation: HKCorrelation, unitMap: [HKQuantity return CorrelationSample( correlationType: CorrelationTypeIdentifier(fromString: correlation.correlationType.identifier)!, objects: objects, - metadataFoodType: correlation.metadata?[HKMetadataKeyFoodType] as? String, sampleType: serializeSampleType(correlation.sampleType), startDate: correlation.startDate, endDate: correlation.endDate, hasUndeterminedDuration: correlation.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - correlation.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: correlation.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: correlation.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - correlation.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - correlation.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(correlation.metadata), uuid: correlation.uuid.uuidString, sourceRevision: serializeSourceRevision(correlation.sourceRevision), - device: serializeDevice(hkDevice: correlation.device), - metadata: serializeMetadata(correlation.metadata), - - metadataExternalUUID: correlation.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: correlation.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: correlation.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: correlation.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: correlation.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: correlation.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: correlation.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: correlation.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: correlation.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: correlation.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: correlation.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: correlation.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: correlation.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: correlation.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: correlation.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: correlation.device) ) } diff --git a/packages/react-native-healthkit/ios/ElectrocardiogramModule.swift b/packages/react-native-healthkit/ios/ElectrocardiogramModule.swift index bb131c23..6c9d4b79 100644 --- a/packages/react-native-healthkit/ios/ElectrocardiogramModule.swift +++ b/packages/react-native-healthkit/ios/ElectrocardiogramModule.swift @@ -38,42 +38,10 @@ private func serializeECGSample(sample: HKElectrocardiogram, includeVoltages: Bo startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(sample.metadata), uuid: sample.uuid.uuidString, sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), - metadata: serializeMetadata(sample.metadata), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: sample.device) ) } diff --git a/packages/react-native-healthkit/ios/HeartbeatSeriesModule.swift b/packages/react-native-healthkit/ios/HeartbeatSeriesModule.swift index e89d9c47..b47d3cd0 100644 --- a/packages/react-native-healthkit/ios/HeartbeatSeriesModule.swift +++ b/packages/react-native-healthkit/ios/HeartbeatSeriesModule.swift @@ -11,42 +11,10 @@ func serializeHeartbeatSeriesSample(sample: HKHeartbeatSeriesSample) async throw startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(sample.metadata), uuid: sample.uuid.uuidString, sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), - metadata: serializeMetadata(sample.metadata), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: sample.device) ) } diff --git a/packages/react-native-healthkit/ios/MedicationModule.swift b/packages/react-native-healthkit/ios/MedicationModule.swift index 39369c65..834aa60c 100644 --- a/packages/react-native-healthkit/ios/MedicationModule.swift +++ b/packages/react-native-healthkit/ios/MedicationModule.swift @@ -97,21 +97,6 @@ import NitroModules startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - - uuid: sample.uuid.uuidString, - sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), metadata: { var meta = serializeMetadata(sample.metadata) if let nick = info?.nickname { @@ -119,26 +104,9 @@ import NitroModules } return meta }(), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + uuid: sample.uuid.uuidString, + sourceRevision: serializeSourceRevision(sample.sourceRevision), + device: serializeDevice(hkDevice: sample.device) ) } diff --git a/packages/react-native-healthkit/ios/PredicateHelpers.swift b/packages/react-native-healthkit/ios/PredicateHelpers.swift index c2f3daa1..f3c3d65b 100644 --- a/packages/react-native-healthkit/ios/PredicateHelpers.swift +++ b/packages/react-native-healthkit/ios/PredicateHelpers.swift @@ -71,10 +71,10 @@ func getPredicateForWorkoutBase(_ filter: FilterForWorkouts?) -> FilterForWorkou return FilterForWorkoutsBase( workoutActivityType: filter.workoutActivityType, duration: filter.duration, - sources: filter.sources, + metadata: filter.metadata, uuid: filter.uuid, + sources: filter.sources, uuids: filter.uuids, - metadata: filter.metadata, date: filter.date, ) } diff --git a/packages/react-native-healthkit/ios/QuantityTypeModule.swift b/packages/react-native-healthkit/ios/QuantityTypeModule.swift index cdbac0bb..9a3cc9bd 100644 --- a/packages/react-native-healthkit/ios/QuantityTypeModule.swift +++ b/packages/react-native-healthkit/ios/QuantityTypeModule.swift @@ -19,7 +19,7 @@ func emptyStatisticsResponse(from: Date?, to: Date?) -> QueryStatisticsResponse func queryStatisticsForQuantityInternal( quantityType: HKQuantityType, statistics: [StatisticsOptions], - options: StatisticsQueryOptions? + options: StatisticsQueryOptionsWithStringUnit? ) async throws -> HKStatistics? { let predicate = createPredicateForSamples(options?.filter) @@ -128,7 +128,7 @@ func queryStatisticsCollectionForQuantityInternal( statistics: [StatisticsOptions], anchorDate: Date, intervalComponents: IntervalComponents, - options: StatisticsQueryOptions? + options: StatisticsQueryOptionsWithStringUnit? ) async throws -> HKStatisticsCollection? { let predicate = createPredicateForSamples(options?.filter) @@ -245,15 +245,15 @@ func serializeStatisticsPerSource(gottenStats: HKStatistics, unit: HKUnit) return QueryStatisticsResponseFromSingleSource( source: serializeSource(source), + startDate: gottenStats.startDate, + endDate: gottenStats.endDate, duration: duration, averageQuantity: averageQuantity, maximumQuantity: maximumQuantity, minimumQuantity: minimumQuantity, sumQuantity: sumQuantity, mostRecentQuantity: mostRecentQuantity, - mostRecentQuantityDateInterval: mostRecentQuantityDateInterval, - startDate: gottenStats.startDate, - endDate: gottenStats.endDate + mostRecentQuantityDateInterval: mostRecentQuantityDateInterval ) } } @@ -308,7 +308,7 @@ func handleHKNoDataOrThrow( class QuantityTypeModule: HybridQuantityTypeModuleSpec { func queryStatisticsForQuantitySeparateBySource( identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], - options: StatisticsQueryOptions? + options: StatisticsQueryOptionsWithStringUnit? ) -> Promise<[QueryStatisticsResponseFromSingleSource]> { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) @@ -331,7 +331,7 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec { func queryStatisticsCollectionForQuantitySeparateBySource( identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], anchorDate: Date, - intervalComponents: IntervalComponents, options: StatisticsQueryOptions? + intervalComponents: IntervalComponents, options: StatisticsQueryOptionsWithStringUnit? ) -> Promise<[QueryStatisticsResponseFromSingleSource]> { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) @@ -380,7 +380,7 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec { func queryStatisticsForQuantity( identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], - options: StatisticsQueryOptions? + options: StatisticsQueryOptionsWithStringUnit? ) -> Promise { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) @@ -407,7 +407,7 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec { func queryStatisticsCollectionForQuantity( identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], anchorDate: Date, - intervalComponents: IntervalComponents, options: StatisticsQueryOptions? + intervalComponents: IntervalComponents, options: StatisticsQueryOptionsWithStringUnit? ) -> Promise<[QueryStatisticsResponse]> { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) @@ -434,7 +434,7 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec { } func queryQuantitySamplesWithAnchor( - identifier: QuantityTypeIdentifier, options: QueryOptionsWithAnchorAndUnit + identifier: QuantityTypeIdentifier, options: QueryOptionsWithAnchorAndStringUnit ) -> Promise { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) @@ -505,7 +505,7 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec { } func queryQuantitySamples( - identifier: QuantityTypeIdentifier, options: QueryOptionsWithSortOrderAndUnit + identifier: QuantityTypeIdentifier, options: QueryOptionsWithSortOrderAndStringUnit ) -> Promise<[QuantitySample]> { return Promise.async { let quantityType = try initializeQuantityType(identifier.stringValue) diff --git a/packages/react-native-healthkit/ios/Serializers.swift b/packages/react-native-healthkit/ios/Serializers.swift index 554c6b96..438bce1a 100644 --- a/packages/react-native-healthkit/ios/Serializers.swift +++ b/packages/react-native-healthkit/ios/Serializers.swift @@ -9,6 +9,12 @@ import Foundation import HealthKit import NitroModules +private let metadataDateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter +}() + func serializeQuantityTyped(unit: HKUnit, quantityNullable: HKQuantity?) -> Quantity? { guard let q = quantityNullable else { return nil @@ -20,28 +26,6 @@ func serializeQuantityTyped(unit: HKUnit, quantityNullable: HKQuantity?) -> Quan ) } -func serializeWeatherCondition(_ hkWeatherCondition: HKWeatherCondition?) -> WeatherCondition? { - guard let hkWeatherCondition = hkWeatherCondition else { - return nil - } - return WeatherCondition(rawValue: Int32(hkWeatherCondition.rawValue)) -} - -func serializeInsulinDeliveryReason(_ hkReason: HKInsulinDeliveryReason?) -> InsulinDeliveryReason? { - guard let hkReason = hkReason else { - return nil - } - return InsulinDeliveryReason(rawValue: Int32(hkReason.rawValue)) -} - -func serializeHeartRateMotionContext(_ hkContext: HKHeartRateMotionContext?) - -> HeartRateMotionContext? { - guard let hkContext = hkContext else { - return nil - } - return HeartRateMotionContext(rawValue: Int32(hkContext.rawValue)) -} - func serializeQuantityTyped(unit: HKUnit, quantity: HKQuantity) -> Quantity { return Quantity( unit: unit.unitString, @@ -59,42 +43,10 @@ func serializeQuantitySample(sample: HKQuantitySample, unit: HKUnit) throws -> Q startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(sample.metadata), uuid: sample.uuid.uuidString, sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), - metadata: serializeMetadata(sample.metadata), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: sample.device) ) } throw runtimeErrorWithPrefix( @@ -116,42 +68,10 @@ func serializeCategorySample(sample: HKCategorySample) -> CategorySample { startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(sample.metadata), uuid: sample.uuid.uuidString, sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), - metadata: serializeMetadata(sample.metadata), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: sample.device) ) } @@ -182,6 +102,16 @@ func serializeUnknownQuantityTyped(quantity: HKQuantity?) -> Quantity? { return serializeQuantityTyped(unit: HKUnit.count(), quantity: quantity) } + let countPerMinuteUnit = HKUnit(from: "count/min") + if quantity.is(compatibleWith: countPerMinuteUnit) { + return serializeQuantityTyped(unit: countPerMinuteUnit, quantity: quantity) + } + + let countPerSecondUnit = HKUnit(from: "count/s") + if quantity.is(compatibleWith: countPerSecondUnit) { + return serializeQuantityTyped(unit: countPerSecondUnit, quantity: quantity) + } + if quantity.is(compatibleWith: HKUnit.meter()) { return serializeQuantityTyped(unit: HKUnit.meter(), quantity: quantity) } @@ -210,6 +140,14 @@ func serializeUnknownQuantityTyped(quantity: HKQuantity?) -> Quantity? { return serializeQuantityTyped(unit: HKUnit.hertz(), quantity: quantity) } + let decibelAWeightedSoundPressureLevelUnit = HKUnit(from: "dBASPL") + if quantity.is(compatibleWith: decibelAWeightedSoundPressureLevelUnit) { + return serializeQuantityTyped( + unit: decibelAWeightedSoundPressureLevelUnit, + quantity: quantity + ) + } + if quantity.is(compatibleWith: SpeedUnit) { return serializeQuantityTyped(unit: SpeedUnit, quantity: quantity) } @@ -218,6 +156,11 @@ func serializeUnknownQuantityTyped(quantity: HKQuantity?) -> Quantity? { return serializeQuantityTyped(unit: METUnit, quantity: quantity) } + let vo2MaxUnit = HKUnit(from: "ml/(kg*min)") + if quantity.is(compatibleWith: vo2MaxUnit) { + return serializeQuantityTyped(unit: vo2MaxUnit, quantity: quantity) + } + if quantity.is(compatibleWith: HKUnit.internationalUnit()) { return serializeQuantityTyped(unit: HKUnit.internationalUnit(), quantity: quantity) } @@ -271,22 +214,47 @@ func serializeMetadata(_ metadata: [String: Any]?) -> AnyMap { let serialized = AnyMap() if let m = metadata { for item in m { + if let number = item.value as? NSNumber { + if isKnownBooleanMetadataKey(item.key) { + serialized.setBoolean(key: item.key, value: number.boolValue) + } else if isKnownNumericMetadataKey(item.key) { + serialized.setDouble(key: item.key, value: number.doubleValue) + } else if CFGetTypeID(number) == CFBooleanGetTypeID() { + serialized.setBoolean(key: item.key, value: number.boolValue) + } else { + serialized.setDouble(key: item.key, value: number.doubleValue) + } + continue + } + if let bool = item.value as? Bool { serialized.setBoolean(key: item.key, value: bool) + continue } if let str = item.value as? String { serialized.setString(key: item.key, value: str) + continue } if let double = item.value as? Double { serialized.setDouble(key: item.key, value: double) + continue + } + + if let date = item.value as? Date { + serialized.setString( + key: item.key, + value: metadataDateFormatter.string(from: date) + ) + continue } if let quantity = item.value as? HKQuantity { if let s = serializeUnknownQuantity(quantity: quantity) { serialized.setObject(key: item.key, value: s) } + continue } if let dict = item.value as? [String: AnyValue] { diff --git a/packages/react-native-healthkit/ios/StateOfMindModule.swift b/packages/react-native-healthkit/ios/StateOfMindModule.swift index b95eb25b..ad8c3d8f 100644 --- a/packages/react-native-healthkit/ios/StateOfMindModule.swift +++ b/packages/react-native-healthkit/ios/StateOfMindModule.swift @@ -40,42 +40,10 @@ import NitroModules startDate: sample.startDate, endDate: sample.endDate, hasUndeterminedDuration: sample.hasUndeterminedDuration, - - metadataWeatherCondition: serializeWeatherCondition( - sample.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition), - metadataWeatherHumidity: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity), - metadataWeatherTemperature: serializeUnknownQuantityTyped( - quantity: sample.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity), - metadataInsulinDeliveryReason: serializeInsulinDeliveryReason( - sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason), - metadataHeartRateMotionContext: serializeHeartRateMotionContext( - sample.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext), - + metadata: serializeMetadata(sample.metadata), uuid: sample.uuid.uuidString, sourceRevision: serializeSourceRevision(sample.sourceRevision), - device: serializeDevice(hkDevice: sample.device), - metadata: serializeMetadata(sample.metadata), - - metadataExternalUUID: sample.metadata?[HKMetadataKeyExternalUUID] as? String, - metadataTimeZone: sample.metadata?[HKMetadataKeyTimeZone] as? String, - metadataWasUserEntered: sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool, - metadataDeviceSerialNumber: sample.metadata?[HKMetadataKeyDeviceSerialNumber] as? String, - metadataUdiDeviceIdentifier: sample.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String, - metadataUdiProductionIdentifier: sample.metadata?[HKMetadataKeyUDIProductionIdentifier] - as? String, - metadataDigitalSignature: sample.metadata?[HKMetadataKeyDigitalSignature] as? String, - metadataDeviceName: sample.metadata?[HKMetadataKeyDeviceName] as? String, - metadataDeviceManufacturerName: sample.metadata?[HKMetadataKeyDeviceManufacturerName] - as? String, - metadataSyncIdentifier: sample.metadata?[HKMetadataKeySyncIdentifier] as? String, - metadataSyncVersion: sample.metadata?[HKMetadataKeySyncVersion] as? Double, - metadataWasTakenInLab: sample.metadata?[HKMetadataKeyWasTakenInLab] as? Bool, - metadataReferenceRangeLowerLimit: sample.metadata?[HKMetadataKeyReferenceRangeLowerLimit] - as? Double, - metadataReferenceRangeUpperLimit: sample.metadata?[HKMetadataKeyReferenceRangeUpperLimit] - as? Double, - metadataAlgorithmVersion: sample.metadata?[HKMetadataKeyAlgorithmVersion] as? Double + device: serializeDevice(hkDevice: sample.device) ) } diff --git a/packages/react-native-healthkit/ios/WorkoutProxy.swift b/packages/react-native-healthkit/ios/WorkoutProxy.swift index 34075bc6..83e11e8d 100644 --- a/packages/react-native-healthkit/ios/WorkoutProxy.swift +++ b/packages/react-native-healthkit/ios/WorkoutProxy.swift @@ -222,40 +222,14 @@ class WorkoutProxy: HybridWorkoutProxySpec { totalFlightsClimbed: self.totalFlightsClimbed, events: self.events, activities: self.activities, - metadataAverageMETs: self.metadataAverageMETs, - metadataElevationAscended: self.metadataElevationAscended, - metadataElevationDescended: self.metadataElevationDescended, - metadataIndoorWorkout: self.metadataIndoorWorkout, - metadataAverageSpeed: self.metadataAverageSpeed, - metadataMaximumSpeed: self.metadataMaximumSpeed, + metadata: self.metadata, sampleType: self.sampleType, startDate: self.startDate, endDate: self.endDate, hasUndeterminedDuration: self.hasUndeterminedDuration, - metadataWeatherCondition: self.metadataWeatherCondition, - metadataWeatherHumidity: self.metadataWeatherHumidity, - metadataWeatherTemperature: self.metadataWeatherTemperature, - metadataInsulinDeliveryReason: self.metadataInsulinDeliveryReason, - metadataHeartRateMotionContext: self.metadataHeartRateMotionContext, uuid: self.uuid, sourceRevision: self.sourceRevision, - device: self.device, - metadata: self.metadata, - metadataExternalUUID: self.metadataExternalUUID, - metadataTimeZone: self.metadataTimeZone, - metadataWasUserEntered: self.metadataWasUserEntered, - metadataDeviceSerialNumber: self.metadataDeviceSerialNumber, - metadataUdiDeviceIdentifier: self.metadataUdiDeviceIdentifier, - metadataUdiProductionIdentifier: self.metadataUdiProductionIdentifier, - metadataDigitalSignature: self.metadataDigitalSignature, - metadataDeviceName: self.metadataDeviceName, - metadataDeviceManufacturerName: self.metadataDeviceManufacturerName, - metadataSyncIdentifier: self.metadataSyncIdentifier, - metadataSyncVersion: self.metadataSyncVersion, - metadataWasTakenInLab: self.metadataWasTakenInLab, - metadataReferenceRangeLowerLimit: self.metadataReferenceRangeLowerLimit, - metadataReferenceRangeUpperLimit: self.metadataReferenceRangeUpperLimit, - metadataAlgorithmVersion: self.metadataAlgorithmVersion + device: self.device ) } @@ -369,126 +343,6 @@ class WorkoutProxy: HybridWorkoutProxySpec { return workout.hasUndeterminedDuration } - // Weather metadata - var metadataWeatherCondition: WeatherCondition? { - return serializeWeatherCondition( - workout.metadata?[HKMetadataKeyWeatherCondition] as? HKWeatherCondition) - } - - var metadataWeatherHumidity: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyWeatherHumidity] as? HKQuantity) - } - - var metadataWeatherTemperature: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyWeatherTemperature] as? HKQuantity) - } - - var metadataInsulinDeliveryReason: InsulinDeliveryReason? { - return serializeInsulinDeliveryReason( - workout.metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason) - } - - var metadataHeartRateMotionContext: HeartRateMotionContext? { - return serializeHeartRateMotionContext( - workout.metadata?[HKMetadataKeyHeartRateMotionContext] as? HKHeartRateMotionContext) - } - - // Workout-specific metadata - var metadataAverageMETs: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyAverageMETs] as? HKQuantity) - } - - var metadataElevationAscended: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyElevationAscended] as? HKQuantity) - } - - var metadataElevationDescended: Quantity? { - if #available(iOS 18.0, *) { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyElevationDescended] as? HKQuantity) - } - return nil - } - - var metadataIndoorWorkout: Bool? { - return workout.metadata?[HKMetadataKeyIndoorWorkout] as? Bool - } - - var metadataAverageSpeed: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyAverageSpeed] as? HKQuantity) - } - - var metadataMaximumSpeed: Quantity? { - return serializeUnknownQuantityTyped( - quantity: workout.metadata?[HKMetadataKeyMaximumSpeed] as? HKQuantity) - } - - // BaseObject metadata - var metadataExternalUUID: String? { - return workout.metadata?[HKMetadataKeyExternalUUID] as? String - } - - var metadataTimeZone: String? { - return workout.metadata?[HKMetadataKeyTimeZone] as? String - } - - var metadataWasUserEntered: Bool? { - return workout.metadata?[HKMetadataKeyWasUserEntered] as? Bool - } - - var metadataDeviceSerialNumber: String? { - return workout.metadata?[HKMetadataKeyDeviceSerialNumber] as? String - } - - var metadataUdiDeviceIdentifier: String? { - return workout.metadata?[HKMetadataKeyUDIDeviceIdentifier] as? String - } - - var metadataUdiProductionIdentifier: String? { - return workout.metadata?[HKMetadataKeyUDIProductionIdentifier] as? String - } - - var metadataDigitalSignature: String? { - return workout.metadata?[HKMetadataKeyDigitalSignature] as? String - } - - var metadataDeviceName: String? { - return workout.metadata?[HKMetadataKeyDeviceName] as? String - } - - var metadataDeviceManufacturerName: String? { - return workout.metadata?[HKMetadataKeyDeviceManufacturerName] as? String - } - - var metadataSyncIdentifier: String? { - return workout.metadata?[HKMetadataKeySyncIdentifier] as? String - } - - var metadataSyncVersion: Double? { - return workout.metadata?[HKMetadataKeySyncVersion] as? Double - } - - var metadataWasTakenInLab: Bool? { - return workout.metadata?[HKMetadataKeyWasTakenInLab] as? Bool - } - - var metadataReferenceRangeLowerLimit: Double? { - return workout.metadata?[HKMetadataKeyReferenceRangeLowerLimit] as? Double - } - - var metadataReferenceRangeUpperLimit: Double? { - return workout.metadata?[HKMetadataKeyReferenceRangeUpperLimit] as? Double - } - - var metadataAlgorithmVersion: Double? { - return workout.metadata?[HKMetadataKeyAlgorithmVersion] as? Double - } - var events: [WorkoutEvent]? { if let hkWorkoutEvents = workout.workoutEvents { return hkWorkoutEvents.compactMap { event in @@ -555,4 +409,4 @@ class WorkoutProxy: HybridWorkoutProxySpec { return try await getSerializedWorkoutLocations(workout: self.workout) } } -} \ No newline at end of file +} diff --git a/packages/react-native-healthkit/ios/generated/HealthkitGenerated.swift b/packages/react-native-healthkit/ios/generated/HealthkitGenerated.swift new file mode 100644 index 00000000..de4a8af1 --- /dev/null +++ b/packages/react-native-healthkit/ios/generated/HealthkitGenerated.swift @@ -0,0 +1,52 @@ +// AUTO-GENERATED FILE. DO NOT EDIT. +// Source: scripts/generate-healthkit.ts + +import Foundation + +private let healthkitBooleanMetadataKeys: Set = [ + "HKAppleDeviceCalibrated", + "HKAppleFitnessPlusSession", + "HKCoachedWorkout", + "HKGroupFitness", + "HKIndoorWorkout", + "HKMenstrualCycleStart", + "HKQuantityClampedToLowerBound", + "HKQuantityClampedToUpperBound", + "HKSexualActivityProtectionUsed", + "HKWasTakenInLab", + "HKWasUserEntered", +] + +private let healthkitNumericMetadataKeys: Set = [ + "HKActivityType", + "HKAlgorithmVersion", + "HKAppleECGAlgorithmVersion", + "HKBloodGlucoseMealTime", + "HKBodyTemperatureSensorLocation", + "HKCyclingFunctionalThresholdPowerTestType", + "HKDevicePlacementSide", + "HKHeartRateMotionContext", + "HKHeartRateRecoveryActivityType", + "HKHeartRateRecoveryTestType", + "HKHeartRateSensorLocation", + "HKInsulinDeliveryReason", + "HKPhysicalEffortEstimationType", + "HKReferenceRangeLowerLimit", + "HKReferenceRangeUpperLimit", + "HKSwimmingLocationType", + "HKSwimmingStrokeStyle", + "HKSWOLFScore", + "HKSyncVersion", + "HKUserMotionContext", + "HKVO2MaxTestType", + "HKWaterSalinity", + "HKWeatherCondition", +] + +func isKnownBooleanMetadataKey(_ key: String) -> Bool { + healthkitBooleanMetadataKeys.contains(key) +} + +func isKnownNumericMetadataKey(_ key: String) -> Bool { + healthkitNumericMetadataKeys.contains(key) +} diff --git a/packages/react-native-healthkit/package.json b/packages/react-native-healthkit/package.json index 07e63a47..0a7e5caa 100644 --- a/packages/react-native-healthkit/package.json +++ b/packages/react-native-healthkit/package.json @@ -52,13 +52,16 @@ "README.md" ], "scripts": { + "generate:healthkit": "bun ./scripts/generate-healthkit-cli.ts", + "verify:healthkit-sdk": "bun ./scripts/verify-healthkit-sdk.ts", "clean": "rm -rf lib", "build:cjs": "tsc -p tsconfig.cjs.json", "build:esm": "tsc -p tsconfig.esm.json", "build:types": "tsc -p tsconfig.declarations.json", "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", - "typecheck": "tsc --noEmit -p tsconfig.json", + "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.type-tests.json", "lint": "bunx @biomejs/biome format --write", + "check:generated": "bun run generate:healthkit && bun run verify:healthkit-sdk && git diff --exit-code -- src/generated/healthkit.generated.ts src/generated/healthkit-schema.json ../../apps/example/contracts/generated ios/generated", "codegen": "bun run build && nitrogen --logLevel=\"debug\" && perl -i -pe 's/Bool\\(fromCxx: cachedCxxPart\\)/cachedCxxPart.use_count() > 0/g' nitrogen/generated/ios/swift/*Spec_cxx.swift", "test": "bun test", "prepublishOnly": "bun run build && bun run codegen" diff --git a/packages/react-native-healthkit/scripts/generate-healthkit-cli.ts b/packages/react-native-healthkit/scripts/generate-healthkit-cli.ts new file mode 100644 index 00000000..2397981a --- /dev/null +++ b/packages/react-native-healthkit/scripts/generate-healthkit-cli.ts @@ -0,0 +1,28 @@ +import { + buildHealthkitSchemaFromSources, + renderGeneratedContracts, + renderGeneratedSwift, + renderGeneratedTypescript, +} from './generate-healthkit' +import { + formatGeneratedArtifacts, + getGeneratedArtifactPaths, + loadHealthkitSdkSources, + writeGeneratedArtifacts, +} from './healthkit-sdk' + +function main() { + const generatedArtifactPaths = getGeneratedArtifactPaths() + const sources = loadHealthkitSdkSources() + const schema = buildHealthkitSchemaFromSources(sources) + writeGeneratedArtifacts( + schema, + renderGeneratedTypescript(schema), + renderGeneratedContracts(schema), + renderGeneratedSwift(schema), + generatedArtifactPaths, + ) + formatGeneratedArtifacts(generatedArtifactPaths) +} + +main() diff --git a/packages/react-native-healthkit/scripts/generate-healthkit.ts b/packages/react-native-healthkit/scripts/generate-healthkit.ts new file mode 100644 index 00000000..b58452db --- /dev/null +++ b/packages/react-native-healthkit/scripts/generate-healthkit.ts @@ -0,0 +1,2118 @@ +import ts from 'typescript' + +type ObjectType = + | 'common' + | 'sample' + | 'categorySample' + | 'quantitySample' + | 'workout' + | 'workoutEvent' + +interface EnumMemberSchema { + readonly name: string + readonly swiftName: string + readonly rawValue: number +} + +interface EnumSchema { + readonly name: string + readonly ios: string | null + readonly members: readonly EnumMemberSchema[] +} + +interface QuantityIdentifierSchema { + readonly name: string + readonly ios: string | null + readonly canonicalUnit: string | null + readonly aggregationStyle: string | null + readonly writeable: boolean + readonly legacy: boolean +} + +interface CategoryIdentifierSchema { + readonly name: string + readonly ios: string | null + readonly valueEnum: string | null + readonly writeable: boolean + readonly legacy: boolean +} + +type MetadataValueKind = 'string' | 'boolean' | 'number' | 'quantity' | 'enum' + +interface MetadataKeySchema { + readonly keyConstant: string + readonly rawKey: string + readonly ios: string | null + readonly expectedType: string | null + readonly tsType: string + readonly valueKind: MetadataValueKind + readonly enumName: string | null + readonly objectTypes: readonly ObjectType[] + readonly identifiers: readonly string[] +} + +export interface HealthkitSchema { + readonly quantityIdentifiers: readonly QuantityIdentifierSchema[] + readonly categoryIdentifiers: readonly CategoryIdentifierSchema[] + readonly enums: readonly EnumSchema[] + readonly metadataKeys: readonly MetadataKeySchema[] +} + +interface MetadataOverride { + readonly objectTypes?: readonly ObjectType[] + readonly identifiers?: readonly string[] + readonly tsType?: string + readonly valueKind?: MetadataValueKind + readonly enumName?: string | null + readonly skip?: boolean +} + +export interface IdentifierOverrides { + readonly quantity: { + readonly readOnly: readonly string[] + } + readonly category: { + readonly readOnly: readonly string[] + } +} + +interface SymbolGraphAvailabilityItem { + readonly domain?: string + readonly introduced?: { + readonly major?: number + readonly minor?: number + } + readonly deprecated?: { + readonly major?: number + readonly minor?: number + } + readonly renamed?: string +} + +interface SymbolGraphSymbol { + readonly kind?: { + readonly identifier?: string + } + readonly identifier?: { + readonly precise?: string + } + readonly pathComponents?: readonly string[] + readonly names?: { + readonly title?: string + } + readonly availability?: readonly SymbolGraphAvailabilityItem[] +} + +export interface SymbolGraphDocument { + readonly symbols: readonly SymbolGraphSymbol[] +} + +interface SymbolGraphNamedConstant { + readonly name: string + readonly ios: string | null + readonly legacy: boolean +} + +const METADATA_OVERRIDES: Record = { + HKMetadataKeyDeviceSerialNumber: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyUDIDeviceIdentifier: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyUDIProductionIdentifier: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyDigitalSignature: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyExternalUUID: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeySyncIdentifier: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeySyncVersion: { + objectTypes: ['common'], + tsType: 'number', + valueKind: 'number', + enumName: null, + }, + HKMetadataKeyTimeZone: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyDeviceName: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyDeviceManufacturerName: { + objectTypes: ['common'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyWasTakenInLab: { + objectTypes: ['common'], + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + }, + HKMetadataKeyReferenceRangeLowerLimit: { + objectTypes: ['common'], + tsType: 'number', + valueKind: 'number', + enumName: null, + }, + HKMetadataKeyReferenceRangeUpperLimit: { + objectTypes: ['common'], + tsType: 'number', + valueKind: 'number', + enumName: null, + }, + HKMetadataKeyWasUserEntered: { + objectTypes: ['common'], + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + }, + HKMetadataKeyWeatherCondition: { + objectTypes: ['sample', 'workout'], + tsType: 'WeatherCondition', + valueKind: 'enum', + enumName: 'WeatherCondition', + }, + HKMetadataKeyWeatherTemperature: { + objectTypes: ['sample', 'workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyWeatherHumidity: { + objectTypes: ['sample', 'workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyActivityType: { + objectTypes: ['sample'], + tsType: 'WorkoutActivityType', + valueKind: 'enum', + enumName: 'WorkoutActivityType', + }, + HKMetadataKeyAlgorithmVersion: { + objectTypes: ['sample'], + tsType: 'number', + valueKind: 'number', + enumName: null, + }, + HKMetadataKeyUserMotionContext: { + objectTypes: ['sample'], + tsType: 'UserMotionContext', + valueKind: 'enum', + enumName: 'UserMotionContext', + }, + HKMetadataKeyAverageMETs: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyAverageSpeed: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyMaximumSpeed: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyAlpineSlopeGrade: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyElevationAscended: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyElevationDescended: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyDateOfEarliestDataUsedForEstimate: { + objectTypes: ['sample'], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyGlassesPrescriptionDescription: { + objectTypes: [], + tsType: 'string', + valueKind: 'string', + enumName: null, + }, + HKMetadataKeyAppleDeviceCalibrated: { + objectTypes: ['sample'], + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + }, + HKMetadataKeySexualActivityProtectionUsed: { + objectTypes: ['categorySample'], + identifiers: ['HKCategoryTypeIdentifierSexualActivity'], + }, + HKMetadataKeyMenstrualCycleStart: { + objectTypes: ['categorySample'], + identifiers: ['HKCategoryTypeIdentifierMenstrualFlow'], + }, + HKMetadataKeyBodyTemperatureSensorLocation: { + objectTypes: ['quantitySample'], + identifiers: [ + 'HKQuantityTypeIdentifierBodyTemperature', + 'HKQuantityTypeIdentifierBasalBodyTemperature', + ], + tsType: 'BodyTemperatureSensorLocation', + valueKind: 'enum', + enumName: 'BodyTemperatureSensorLocation', + }, + HKMetadataKeyHeartRateSensorLocation: { + objectTypes: ['quantitySample'], + tsType: 'HeartRateSensorLocation', + valueKind: 'enum', + enumName: 'HeartRateSensorLocation', + }, + HKMetadataKeyHeartRateMotionContext: { + objectTypes: ['quantitySample'], + tsType: 'HeartRateMotionContext', + valueKind: 'enum', + enumName: 'HeartRateMotionContext', + }, + HKMetadataKeyHeartRateEventThreshold: { + objectTypes: ['categorySample'], + identifiers: [ + 'HKCategoryTypeIdentifierHighHeartRateEvent', + 'HKCategoryTypeIdentifierLowHeartRateEvent', + ], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeySessionEstimate: { + objectTypes: ['quantitySample'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyHeartRateRecoveryTestType: { + objectTypes: ['quantitySample'], + tsType: 'HeartRateRecoveryTestType', + valueKind: 'enum', + enumName: 'HeartRateRecoveryTestType', + }, + HKMetadataKeyHeartRateRecoveryActivityType: { + objectTypes: ['quantitySample'], + tsType: 'WorkoutActivityType', + valueKind: 'enum', + enumName: 'WorkoutActivityType', + }, + HKMetadataKeyHeartRateRecoveryActivityDuration: { + objectTypes: ['quantitySample'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate: { + objectTypes: ['quantitySample'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyBloodGlucoseMealTime: { + objectTypes: ['quantitySample'], + identifiers: ['HKQuantityTypeIdentifierBloodGlucose'], + tsType: 'BloodGlucoseMealTime', + valueKind: 'enum', + enumName: 'BloodGlucoseMealTime', + }, + HKMetadataKeyVO2MaxTestType: { + objectTypes: ['quantitySample'], + identifiers: ['HKQuantityTypeIdentifierVO2Max'], + tsType: 'VO2MaxTestType', + valueKind: 'enum', + enumName: 'VO2MaxTestType', + }, + HKMetadataKeyInsulinDeliveryReason: { + objectTypes: ['quantitySample'], + identifiers: ['HKQuantityTypeIdentifierInsulinDelivery'], + tsType: 'InsulinDeliveryReason', + valueKind: 'enum', + enumName: 'InsulinDeliveryReason', + }, + HKMetadataKeyQuantityClampedToLowerBound: { + objectTypes: ['quantitySample'], + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + }, + HKMetadataKeyQuantityClampedToUpperBound: { + objectTypes: ['quantitySample'], + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + }, + HKMetadataKeyAudioExposureLevel: { + objectTypes: ['categorySample'], + identifiers: [ + 'HKCategoryTypeIdentifierAudioExposureEvent', + 'HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent', + 'HKCategoryTypeIdentifierHeadphoneAudioExposureEvent', + ], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyVO2MaxValue: { + objectTypes: ['categorySample'], + identifiers: ['HKCategoryTypeIdentifierLowCardioFitnessEvent'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyLowCardioFitnessEventThreshold: { + objectTypes: ['categorySample'], + identifiers: ['HKCategoryTypeIdentifierLowCardioFitnessEvent'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyHeadphoneGain: { + objectTypes: ['categorySample'], + identifiers: [ + 'HKCategoryTypeIdentifierAudioExposureEvent', + 'HKCategoryTypeIdentifierHeadphoneAudioExposureEvent', + ], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyWorkoutBrandName: { + objectTypes: ['workout'], + }, + HKMetadataKeyGroupFitness: { + objectTypes: ['workout'], + }, + HKMetadataKeyAppleFitnessPlusCatalogIdentifier: { + objectTypes: ['workout'], + }, + HKMetadataKeyAppleFitnessPlusSession: { + objectTypes: ['workout'], + }, + HKMetadataKeyIndoorWorkout: { + objectTypes: ['workout'], + }, + HKMetadataKeyCoachedWorkout: { + objectTypes: ['workout'], + }, + HKMetadataKeyLapLength: { + objectTypes: ['workout'], + }, + HKMetadataKeySwimmingLocationType: { + objectTypes: ['workout'], + tsType: 'WorkoutSwimmingLocationType', + valueKind: 'enum', + enumName: 'WorkoutSwimmingLocationType', + }, + HKMetadataKeySwimmingStrokeStyle: { + objectTypes: ['workoutEvent'], + tsType: 'SwimmingStrokeStyle', + valueKind: 'enum', + enumName: 'SwimmingStrokeStyle', + }, + HKMetadataKeySWOLFScore: { + objectTypes: ['workout'], + tsType: 'number', + valueKind: 'number', + enumName: null, + }, + HKMetadataKeyCrossTrainerDistance: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyFitnessMachineDuration: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyIndoorBikeDistance: { + objectTypes: ['workout'], + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + }, + HKMetadataKeyPhysicalEffortEstimationType: { + objectTypes: ['sample'], + tsType: 'PhysicalEffortEstimationType', + valueKind: 'enum', + enumName: 'PhysicalEffortEstimationType', + }, + HKMetadataKeyWaterSalinity: { + objectTypes: ['sample'], + tsType: 'WaterSalinity', + valueKind: 'enum', + enumName: 'WaterSalinity', + }, +} + +const CATEGORY_VALUE_ENUM_OVERRIDES: Record = { + HKCategoryTypeIdentifierMenstrualFlow: 'HKCategoryValueMenstrualFlow', + HKCategoryTypeIdentifierAudioExposureEvent: + 'HKCategoryValueEnvironmentalAudioExposureEvent', +} + +const EXISTING_ENUM_NAME_OVERRIDES: Record = { + HKWeatherCondition: 'WeatherCondition', + HKHeartRateMotionContext: 'HeartRateMotionContext', + HKInsulinDeliveryReason: 'InsulinDeliveryReason', +} + +function lowerFirst(value: string): string { + if (!value) { + return value + } + + return `${value.charAt(0).toLowerCase()}${value.slice(1)}` +} + +function toTsEnumName(swiftName: string): string { + return EXISTING_ENUM_NAME_OVERRIDES[swiftName] ?? swiftName.replace(/^HK/, '') +} + +function toRawMetadataKey(keyConstant: string): string { + return keyConstant.replace(/^HKMetadataKey/, 'HK') +} + +function splitTopLevelCommas(input: string): string[] { + const result: string[] = [] + let current = '' + let depth = 0 + + for (const char of input) { + if (char === '(') { + depth += 1 + } else if (char === ')') { + depth = Math.max(0, depth - 1) + } + + if (char === ',' && depth === 0) { + if (current.trim()) { + result.push(current.trim()) + } + current = '' + continue + } + + current += char + } + + if (current.trim()) { + result.push(current.trim()) + } + + return result +} + +function parseIosAvailability(text: string): string | null { + const match = text.match(/ios\(([^),]+)/) + return match?.[1]?.trim() ?? null +} + +function parseSymbolGraphIosAvailability( + symbol: SymbolGraphSymbol, +): string | null { + const iosAvailability = symbol.availability?.find( + (entry) => entry.domain === 'iOS', + ) + const major = iosAvailability?.introduced?.major + if (major == null) { + return null + } + const minor = iosAvailability?.introduced?.minor ?? 0 + return `${major}.${minor}` +} + +function isLegacySymbol(symbol: SymbolGraphSymbol): boolean { + return ( + symbol.availability?.some( + (entry) => entry.deprecated != null || entry.renamed != null, + ) ?? false + ) +} + +function parseSwiftNamedConstants( + symbolGraph: SymbolGraphDocument, + ownerName: string, + precisePrefix?: string, +): SymbolGraphNamedConstant[] { + return symbolGraph.symbols + .filter((symbol) => symbol.kind?.identifier === 'swift.type.property') + .filter((symbol) => symbol.pathComponents?.[0] === ownerName) + .filter((symbol) => + precisePrefix == null + ? true + : (symbol.identifier?.precise?.startsWith(precisePrefix) ?? false), + ) + .map((symbol) => { + const precise = symbol.identifier?.precise + const name = precise?.replace(/^c:@/, '') + if (name == null) { + throw new Error(`Missing precise identifier for ${ownerName} constant`) + } + + return { + name, + ios: parseSymbolGraphIosAvailability(symbol), + legacy: isLegacySymbol(symbol), + } + }) + .sort((left, right) => left.name.localeCompare(right.name)) +} + +function sanitizeEnumEntry(entry: string): string { + return entry + .replace(/\/\/.*$/gm, '') + .replace(/\s+/g, ' ') + .trim() +} + +function parseNumericValue( + rawValue: string | undefined, + knownValues: Map, + previousValue: number, +): number { + if (rawValue == null || rawValue === '') { + return previousValue + 1 + } + + const cleanValue = rawValue.trim() + if (/^-?\d+$/.test(cleanValue)) { + return Number.parseInt(cleanValue, 10) + } + + if (knownValues.has(cleanValue)) { + const knownValue = knownValues.get(cleanValue) + if (knownValue != null) { + return knownValue + } + } + + return previousValue + 1 +} + +export function parseNsEnums(headerSource: string): EnumSchema[] { + const enums: EnumSchema[] = [] + const enumRegex = + /typedef NS_ENUM\((?:NS)?(?:U)?Integer,\s*(\w+)\)\s*\{([\s\S]*?)\}\s*([^;]*);/g + + for (const match of headerSource.matchAll(enumRegex)) { + const swiftName = match[1] + const rawBody = match[2] + if (swiftName == null || rawBody == null) { + continue + } + const body = rawBody.replace(/\/\/.*$/gm, '') + const trailer = match[3] ?? '' + const ios = parseIosAvailability(trailer) + const entries = splitTopLevelCommas(body) + const members: EnumMemberSchema[] = [] + const knownValues = new Map() + let previousValue = -1 + + for (const rawEntry of entries) { + const entry = sanitizeEnumEntry(rawEntry) + if (!entry) { + continue + } + + const nameMatch = entry.match(/^(\w+)/) + if (!nameMatch) { + continue + } + + const fullName = nameMatch[1] + if (fullName == null) { + continue + } + const suffix = fullName.startsWith(swiftName) + ? fullName.slice(swiftName.length) + : fullName.replace(/^HK/, '') + const memberName = lowerFirst( + /^\d/.test(suffix) ? `value${suffix}` : suffix, + ) + const rawValueMatch = entry.match(/=\s*([^=]+)$/) + const rawValue = parseNumericValue( + rawValueMatch?.[1], + knownValues, + previousValue, + ) + previousValue = rawValue + knownValues.set(fullName, rawValue) + + members.push({ + name: memberName, + swiftName: fullName, + rawValue, + }) + } + + enums.push({ + name: toTsEnumName(swiftName), + ios, + members, + }) + } + + return enums.sort((left, right) => left.name.localeCompare(right.name)) +} + +function parseQuantityIdentifierComments(headerSource: string): Map< + string, + { + readonly canonicalUnit: string | null + readonly aggregationStyle: string | null + } +> { + const identifiers = new Map< + string, + { + readonly canonicalUnit: string | null + readonly aggregationStyle: string | null + } + >() + const regex = + /^HK_EXTERN HKQuantityTypeIdentifier const (\w+)\s+([^;]*);\s*\/\/\s*(.*)$/gm + + for (const match of headerSource.matchAll(regex)) { + const name = match[1] + if (name == null) { + continue + } + const comment = match[3]?.trim() ?? '' + const [canonicalUnitRaw, aggregationRaw] = comment + .split(',') + .map((value) => value.trim()) + identifiers.set(name, { + canonicalUnit: canonicalUnitRaw || null, + aggregationStyle: aggregationRaw || null, + }) + } + + return identifiers +} + +function parseCategoryIdentifierValueEnums( + headerSource: string, +): Map { + const identifiers = new Map() + const regex = + /^HK_EXTERN HKCategoryTypeIdentifier const (\w+)\s+([^;]*);\s*\/\/\s*(.*)$/gm + + for (const match of headerSource.matchAll(regex)) { + const name = match[1] + if (name == null) { + continue + } + const comment = match[3]?.trim() ?? '' + identifiers.set( + name, + CATEGORY_VALUE_ENUM_OVERRIDES[name] ?? (comment || null), + ) + } + + return identifiers +} + +function parseMetadataDocComments(headerSource: string): Map< + string, + { + readonly expectedType: string | null + readonly objectTypes: readonly ObjectType[] + readonly identifiers: readonly string[] + } +> { + const keys = new Map< + string, + { + readonly expectedType: string | null + readonly objectTypes: readonly ObjectType[] + readonly identifiers: readonly string[] + } + >() + const regex = + /(\/\*![\s\S]*?\*\/)\s*HK_EXTERN NSString \* const (HKMetadataKey\w+)\s*([^;]*);/g + + for (const match of headerSource.matchAll(regex)) { + const docComment = match[1] + const keyConstant = match[2] + if (docComment == null || keyConstant == null) { + continue + } + + keys.set(keyConstant, { + expectedType: extractExpectedType(docComment), + objectTypes: inferObjectTypes(docComment).objectTypes, + identifiers: inferObjectTypes(docComment).identifiers, + }) + } + + return keys +} + +function parseQuantityIdentifiers( + symbolGraph: SymbolGraphDocument, + headerSource: string, + overrides: IdentifierOverrides, +): QuantityIdentifierSchema[] { + const readOnly = new Set(overrides.quantity.readOnly) + const commentMap = parseQuantityIdentifierComments(headerSource) + const identifiers = new Map() + const discovered = parseSwiftNamedConstants( + symbolGraph, + 'HKQuantityTypeIdentifier', + 'c:@HKQuantityTypeIdentifier', + ) + + for (const constant of discovered) { + const commentInfo = commentMap.get(constant.name) + identifiers.set(constant.name, { + name: constant.name, + ios: constant.ios, + canonicalUnit: commentInfo?.canonicalUnit ?? null, + aggregationStyle: commentInfo?.aggregationStyle ?? null, + writeable: !readOnly.has(constant.name), + legacy: constant.legacy, + }) + } + + return [...identifiers.values()].sort((left, right) => + left.name.localeCompare(right.name), + ) +} + +function parseCategoryIdentifiers( + symbolGraph: SymbolGraphDocument, + headerSource: string, + overrides: IdentifierOverrides, +): CategoryIdentifierSchema[] { + const readOnly = new Set(overrides.category.readOnly) + const valueEnumMap = parseCategoryIdentifierValueEnums(headerSource) + const identifiers = new Map() + const discovered = parseSwiftNamedConstants( + symbolGraph, + 'HKCategoryTypeIdentifier', + 'c:@HKCategoryTypeIdentifier', + ) + + for (const constant of discovered) { + identifiers.set(constant.name, { + name: constant.name, + ios: constant.ios, + valueEnum: valueEnumMap.get(constant.name) ?? null, + writeable: !readOnly.has(constant.name), + legacy: constant.legacy, + }) + } + + return [...identifiers.values()].sort((left, right) => + left.name.localeCompare(right.name), + ) +} + +function extractExpectedType(docComment: string): string | null { + const normalized = docComment.replace(/\s+/g, ' ') + const match = normalized.match(/The expected value type is ([^.]+)\./i) + return match?.[1]?.trim() ?? null +} + +function inferObjectTypes(docComment: string): { + readonly objectTypes: ObjectType[] + readonly identifiers: string[] +} { + const normalized = docComment.replace(/\s+/g, ' ') + const identifiers = [ + ...new Set( + [...normalized.matchAll(/HK(?:Category|Quantity)TypeIdentifier\w+/g)].map( + (match) => match[0], + ), + ), + ] + const objectTypes = new Set() + + if ( + /HKObject|creating an HKObject|when the HKObject was created|of the HKObject/i.test( + normalized, + ) + ) { + objectTypes.add('common') + } + + if (/sample|reading|HKSample/i.test(normalized)) { + objectTypes.add('sample') + } + + if (/category sample/i.test(normalized)) { + objectTypes.add('categorySample') + } + + if (/quantity sample|HKQuantitySample/i.test(normalized)) { + objectTypes.add('quantitySample') + } + + if ( + /HKWorkout object|workout segment|distance sample|workout was/i.test( + normalized, + ) + ) { + objectTypes.add('workout') + } + + if (/HKWorkoutEvent object/i.test(normalized)) { + objectTypes.add('workoutEvent') + } + + return { + objectTypes: [...objectTypes], + identifiers, + } +} + +function inferTsType(expectedType: string | null): { + readonly tsType: string + readonly valueKind: MetadataValueKind + readonly enumName: string | null +} | null { + if (expectedType == null) { + return null + } + + if (/NSString/i.test(expectedType)) { + return { + tsType: 'string', + valueKind: 'string', + enumName: null, + } + } + + if (/BOOL/i.test(expectedType)) { + return { + tsType: 'boolean', + valueKind: 'boolean', + enumName: null, + } + } + + if (/HKQuantity/i.test(expectedType)) { + return { + tsType: 'Quantity', + valueKind: 'quantity', + enumName: null, + } + } + + const enumMatch = expectedType.match(/\b(HK[A-Za-z0-9]+)\b/) + + if (enumMatch?.[1] && enumMatch[1] !== 'HKQuantity') { + return { + tsType: toTsEnumName(enumMatch[1]), + valueKind: 'enum', + enumName: toTsEnumName(enumMatch[1]), + } + } + + if (/NSNumber/i.test(expectedType)) { + return { + tsType: 'number', + valueKind: 'number', + enumName: null, + } + } + + return null +} + +function parseMetadataKeys( + symbolGraph: SymbolGraphDocument, + headerSource: string, +): MetadataKeySchema[] { + const keys: MetadataKeySchema[] = [] + const docCommentMap = parseMetadataDocComments(headerSource) + const metadataConstants = symbolGraph.symbols + .filter((symbol) => symbol.kind?.identifier === 'swift.var') + .filter((symbol) => + symbol.identifier?.precise?.startsWith('c:@HKMetadataKey'), + ) + .map((symbol) => { + const precise = symbol.identifier?.precise + const name = precise?.replace(/^c:@/, '') + if (name == null) { + throw new Error('Missing metadata key precise identifier') + } + return { + name, + ios: parseSymbolGraphIosAvailability(symbol), + } + }) + .sort((left, right) => left.name.localeCompare(right.name)) + + for (const constant of metadataConstants) { + const docComment = docCommentMap.get(constant.name) + const keyConstant = constant.name + const ios = constant.ios + const expectedType = docComment?.expectedType ?? null + const inferred = inferTsType(expectedType) + const override = METADATA_OVERRIDES[keyConstant] + + if (override?.skip) { + continue + } + + if (inferred == null && override?.tsType == null) { + continue + } + + const tsType = override?.tsType ?? inferred?.tsType + const valueKind = override?.valueKind ?? inferred?.valueKind + const enumName = + override != null && 'enumName' in override + ? (override.enumName ?? null) + : (inferred?.enumName ?? null) + + if (tsType == null || valueKind == null) { + continue + } + + keys.push({ + keyConstant, + rawKey: toRawMetadataKey(keyConstant), + ios, + expectedType, + tsType, + valueKind, + enumName, + objectTypes: override?.objectTypes ?? docComment?.objectTypes ?? [], + identifiers: override?.identifiers ?? docComment?.identifiers ?? [], + }) + } + + return keys.sort((left, right) => left.rawKey.localeCompare(right.rawKey)) +} + +export function buildHealthkitSchemaFromSources(sources: { + readonly sdkPath: string + readonly symbolGraph: SymbolGraphDocument + readonly identifierOverrides: IdentifierOverrides + readonly typeIdentifiersHeader: string + readonly categoryValuesHeader: string + readonly metadataHeader: string + readonly metadataEnumsHeader: string + readonly workoutHeader: string +}): HealthkitSchema { + const enumsByName = new Map() + for (const enumSchema of [ + ...parseNsEnums(sources.categoryValuesHeader), + ...parseNsEnums(sources.metadataEnumsHeader), + ...parseNsEnums(sources.workoutHeader).filter( + (schema) => + schema.name === 'WorkoutActivityType' || + schema.name === 'WorkoutEventType', + ), + ]) { + enumsByName.set(enumSchema.name, enumSchema) + } + + return { + quantityIdentifiers: parseQuantityIdentifiers( + sources.symbolGraph, + sources.typeIdentifiersHeader, + sources.identifierOverrides, + ), + categoryIdentifiers: parseCategoryIdentifiers( + sources.symbolGraph, + sources.typeIdentifiersHeader, + sources.identifierOverrides, + ), + enums: [...enumsByName.values()].sort((left, right) => + left.name.localeCompare(right.name), + ), + metadataKeys: parseMetadataKeys( + sources.symbolGraph, + sources.metadataHeader, + ), + } +} + +function unique(values: readonly T[]): T[] { + return [...new Set(values)] +} + +function tsSource( + strings: TemplateStringsArray, + ...values: readonly string[] +): string { + let output = '' + + for (const [index, string] of strings.entries()) { + output += string + output += values[index] ?? '' + } + + return output +} + +const FACTORY = ts.factory +const EXPORT_MODIFIER = FACTORY.createModifier(ts.SyntaxKind.ExportKeyword) +const READONLY_MODIFIER = FACTORY.createModifier(ts.SyntaxKind.ReadonlyKeyword) + +function identifierName(name: string): ts.PropertyName { + return FACTORY.createIdentifier(name) +} + +function stringLiteralType(value: string): ts.TypeNode { + return FACTORY.createLiteralTypeNode(FACTORY.createStringLiteral(value)) +} + +function keywordType(kind: ts.KeywordTypeSyntaxKind): ts.KeywordTypeNode { + return FACTORY.createKeywordTypeNode(kind) +} + +function namedType( + name: string, + typeArguments: ts.TypeNode[] = [], +): ts.TypeNode { + switch (name) { + case 'string': + return keywordType(ts.SyntaxKind.StringKeyword) + case 'number': + return keywordType(ts.SyntaxKind.NumberKeyword) + case 'boolean': + return keywordType(ts.SyntaxKind.BooleanKeyword) + case 'never': + return keywordType(ts.SyntaxKind.NeverKeyword) + default: + return FACTORY.createTypeReferenceNode(name, typeArguments) + } +} + +function canonicalUnitToTypeNode(unit: string | null): ts.TypeNode { + if (unit == null) { + return keywordType(ts.SyntaxKind.StringKeyword) + } + + if (unit === 'mg/dL' || unit.startsWith('mmol<')) { + return namedType('BloodGlucoseUnit') + } + if (unit === '%') { + return stringLiteralType('%') + } + if (unit === 'count') { + return stringLiteralType('count') + } + if (unit === 'IU') { + return stringLiteralType('IU') + } + if (unit === 'appleEffortScore') { + return stringLiteralType('appleEffortScore') + } + if (unit === 'degC' || unit === 'degF' || unit === 'K') { + return namedType('TemperatureUnit') + } + if ( + unit === 'm' || + unit === 'cm' || + unit === 'km' || + unit === 'ft' || + unit === 'in' || + unit === 'yd' || + unit === 'mi' + ) { + return namedType('LengthUnit') + } + if ( + unit === 'kg' || + unit === 'g' || + unit === 'mg' || + unit === 'mcg' || + unit === 'oz' || + unit === 'lb' || + unit === 'st' + ) { + return namedType('MassUnit') + } + if (unit === 'l' || unit === 'ml') { + return namedType('VolumeUnit') + } + if (unit === 'kcal' || unit === 'Cal' || unit === 'cal' || unit === 'J') { + return namedType('EnergyUnit') + } + if (unit === 'd' || unit === 'hr' || unit === 'min' || unit === 's') { + return namedType('TimeUnit') + } + if (/^count\/(s|min|hr|d)$/.test(unit)) { + return namedType('CountPerTime', [namedType('TimeUnit')]) + } + if ( + /^(m|km|cm|ft|in|yd|mi)\/(s|min|hr|d)$/.test(unit) || + /^(m|km|cm)\/(s|min|hr)$/.test(unit) + ) { + return namedType('SpeedUnit', [ + namedType('LengthUnit'), + namedType('TimeUnit'), + ]) + } + if ( + unit === 'mmHg' || + unit === 'inHg' || + unit === 'cmAq' || + unit === 'atm' || + unit === 'dBASPL' || + unit === 'Pa' + ) { + return namedType('PressureUnit') + } + if (unit === 'Hz') { + return namedType('Unit') + } + + return keywordType(ts.SyntaxKind.StringKeyword) +} + +function exportedTypeAlias( + name: string, + type: ts.TypeNode, + typeParameters?: ts.TypeParameterDeclaration[], +): ts.TypeAliasDeclaration { + return FACTORY.createTypeAliasDeclaration( + [EXPORT_MODIFIER], + name, + typeParameters, + type, + ) +} + +function exportedInterface( + name: string, + members: ts.TypeElement[], + extendsNames: string[] = [], +): ts.InterfaceDeclaration { + return FACTORY.createInterfaceDeclaration( + [EXPORT_MODIFIER], + name, + undefined, + extendsNames.map((extendName) => + FACTORY.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ + FACTORY.createExpressionWithTypeArguments( + FACTORY.createIdentifier(extendName), + undefined, + ), + ]), + ), + members, + ) +} + +function readonlyProperty( + name: string, + type: ts.TypeNode, + optional = true, +): ts.PropertySignature { + return FACTORY.createPropertySignature( + [READONLY_MODIFIER], + identifierName(name), + optional ? FACTORY.createToken(ts.SyntaxKind.QuestionToken) : undefined, + type, + ) +} + +function exportedConstObject( + name: string, + entries: readonly { readonly key: string; readonly value: string | null }[], +): ts.VariableStatement { + return FACTORY.createVariableStatement( + [EXPORT_MODIFIER], + FACTORY.createVariableDeclarationList( + [ + FACTORY.createVariableDeclaration( + name, + undefined, + undefined, + FACTORY.createAsExpression( + FACTORY.createObjectLiteralExpression( + entries.map((entry) => + FACTORY.createPropertyAssignment( + identifierName(entry.key), + entry.value == null + ? FACTORY.createNull() + : FACTORY.createStringLiteral(entry.value), + ), + ), + true, + ), + FACTORY.createTypeReferenceNode('const'), + ), + ), + ], + ts.NodeFlags.Const, + ), + ) +} + +function createLiteralUnion(values: readonly string[]): ts.TypeNode { + if (values.length === 0) { + return keywordType(ts.SyntaxKind.NeverKeyword) + } + if (values.length === 1) { + const firstValue = values[0] + if (firstValue == null) { + return keywordType(ts.SyntaxKind.NeverKeyword) + } + return stringLiteralType(firstValue) + } + return FACTORY.createUnionTypeNode( + values.map((value) => stringLiteralType(value)), + ) +} + +function createPickType( + baseName: string, + fields: readonly string[], +): ts.TypeNode { + return namedType('Pick', [namedType(baseName), createLiteralUnion(fields)]) +} + +function createKeyofType(name: string): ts.TypeNode { + return FACTORY.createTypeOperatorNode( + ts.SyntaxKind.KeyOfKeyword, + namedType(name), + ) +} + +function createConditionalGeneratedType( + genericName: string, + baseName: string, + mapName: string, + fallbackType: ts.TypeNode, +): ts.TypeAliasDeclaration { + return exportedTypeAlias( + genericName, + FACTORY.createIntersectionTypeNode([ + namedType(baseName), + FACTORY.createParenthesizedType( + FACTORY.createConditionalTypeNode( + namedType('T'), + createKeyofType(mapName), + FACTORY.createIndexedAccessTypeNode( + namedType(mapName), + namedType('T'), + ), + fallbackType, + ), + ), + ]), + [ + FACTORY.createTypeParameterDeclaration( + undefined, + 'T', + FACTORY.createTypeReferenceNode( + genericName.includes('Quantity') + ? 'QuantityTypeIdentifier' + : 'CategoryTypeIdentifier', + ), + FACTORY.createTypeReferenceNode( + genericName.includes('Quantity') + ? 'QuantityTypeIdentifier' + : 'CategoryTypeIdentifier', + ), + ), + ], + ) +} + +export function renderGeneratedTypescript(schema: HealthkitSchema): string { + const statements: ts.Statement[] = [] + + statements.push( + FACTORY.createImportDeclaration( + undefined, + FACTORY.createImportClause( + true, + undefined, + FACTORY.createNamedImports([ + FACTORY.createImportSpecifier( + true, + undefined, + FACTORY.createIdentifier('Quantity'), + ), + ]), + ), + FACTORY.createStringLiteral('../types/QuantityType'), + ), + ) + statements.push( + FACTORY.createImportDeclaration( + undefined, + FACTORY.createImportClause( + true, + undefined, + FACTORY.createNamedImports( + [ + 'BloodGlucoseUnit', + 'CountPerTime', + 'EnergyUnit', + 'LengthUnit', + 'MassUnit', + 'PressureUnit', + 'SpeedUnit', + 'TemperatureUnit', + 'TimeUnit', + ].map((name) => + FACTORY.createImportSpecifier( + true, + undefined, + FACTORY.createIdentifier(name), + ), + ), + ), + ), + FACTORY.createStringLiteral('../types/Units'), + ), + ) + + const quantityReadOnly = schema.quantityIdentifiers + .filter((identifier) => !identifier.writeable) + .map((identifier) => identifier.name) + const quantityWriteable = schema.quantityIdentifiers + .filter((identifier) => identifier.writeable) + .map((identifier) => identifier.name) + const categoryReadOnly = schema.categoryIdentifiers + .filter((identifier) => !identifier.writeable) + .map((identifier) => identifier.name) + const categoryWriteable = schema.categoryIdentifiers + .filter((identifier) => identifier.writeable) + .map((identifier) => identifier.name) + + statements.push( + exportedTypeAlias( + 'QuantityTypeIdentifierReadOnly', + createLiteralUnion(quantityReadOnly), + ), + exportedTypeAlias( + 'QuantityTypeIdentifierWriteable', + createLiteralUnion(quantityWriteable), + ), + exportedTypeAlias( + 'QuantityTypeIdentifier', + FACTORY.createUnionTypeNode([ + namedType('QuantityTypeIdentifierReadOnly'), + namedType('QuantityTypeIdentifierWriteable'), + ]), + ), + exportedTypeAlias( + 'CategoryTypeIdentifierReadOnly', + createLiteralUnion(categoryReadOnly), + ), + exportedTypeAlias( + 'CategoryTypeIdentifierWriteable', + createLiteralUnion(categoryWriteable), + ), + exportedTypeAlias( + 'CategoryTypeIdentifier', + FACTORY.createUnionTypeNode([ + namedType('CategoryTypeIdentifierReadOnly'), + namedType('CategoryTypeIdentifierWriteable'), + ]), + ), + ) + + statements.push( + exportedConstObject( + 'QUANTITY_IDENTIFIER_IOS_AVAILABILITY', + schema.quantityIdentifiers.map((identifier) => ({ + key: identifier.name, + value: identifier.ios, + })), + ), + exportedConstObject( + 'CATEGORY_IDENTIFIER_IOS_AVAILABILITY', + schema.categoryIdentifiers.map((identifier) => ({ + key: identifier.name, + value: identifier.ios, + })), + ), + exportedConstObject( + 'QUANTITY_IDENTIFIER_CANONICAL_UNITS', + schema.quantityIdentifiers.map((identifier) => ({ + key: identifier.name, + value: identifier.canonicalUnit, + })), + ), + exportedConstObject( + 'CATEGORY_IDENTIFIER_VALUE_ENUMS', + schema.categoryIdentifiers.map((identifier) => ({ + key: identifier.name, + value: identifier.valueEnum ? toTsEnumName(identifier.valueEnum) : null, + })), + ), + ) + + statements.push( + ...schema.enums.map((schemaEnum) => + FACTORY.createEnumDeclaration( + [EXPORT_MODIFIER], + schemaEnum.name, + schemaEnum.members.map((member) => + FACTORY.createEnumMember( + member.name, + FACTORY.createNumericLiteral(member.rawValue), + ), + ), + ), + ), + ) + + const categoryValueMembers = schema.categoryIdentifiers + .filter((identifier) => identifier.valueEnum != null) + .map((identifier) => { + if (identifier.valueEnum == null) { + throw new Error(`Missing category value enum for ${identifier.name}`) + } + return readonlyProperty( + identifier.name, + namedType(toTsEnumName(identifier.valueEnum)), + false, + ) + }) + statements.push( + exportedInterface('CategoryValueByIdentifierMap', categoryValueMembers), + exportedTypeAlias( + 'CategoryValueForIdentifierGenerated', + FACTORY.createConditionalTypeNode( + namedType('T'), + createKeyofType('CategoryValueByIdentifierMap'), + FACTORY.createIndexedAccessTypeNode( + namedType('CategoryValueByIdentifierMap'), + namedType('T'), + ), + keywordType(ts.SyntaxKind.NumberKeyword), + ), + [ + FACTORY.createTypeParameterDeclaration( + undefined, + 'T', + namedType('CategoryTypeIdentifier'), + namedType('CategoryTypeIdentifier'), + ), + ], + ), + ) + + const metadataByObjectType = { + common: schema.metadataKeys.filter((key) => + key.objectTypes.includes('common'), + ), + sample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('sample'), + ), + categorySample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('categorySample'), + ), + quantitySample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('quantitySample'), + ), + workout: schema.metadataKeys.filter((key) => + key.objectTypes.includes('workout'), + ), + workoutEvent: schema.metadataKeys.filter((key) => + key.objectTypes.includes('workoutEvent'), + ), + } + + const uniqueMetadataMembers = ( + keys: readonly HealthkitSchema['metadataKeys'][number][], + ) => + unique(keys.map((key) => key.rawKey)).map((rawKey) => { + const key = keys.find((entry) => entry.rawKey === rawKey) + if (key == null) { + throw new Error(`Missing metadata key for raw key ${rawKey}`) + } + return readonlyProperty(key.rawKey, namedType(key.tsType)) + }) + + const quantityGlobalKeys = metadataByObjectType.quantitySample.filter( + (key) => key.identifiers.length === 0, + ) + const quantitySpecificKeys = metadataByObjectType.quantitySample.filter( + (key) => key.identifiers.length > 0, + ) + const categorySpecificKeys = metadataByObjectType.categorySample.filter( + (key) => key.identifiers.length > 0, + ) + + statements.push( + exportedInterface( + 'KnownObjectMetadata', + uniqueMetadataMembers(metadataByObjectType.common), + ), + exportedInterface( + 'KnownSampleMetadata', + uniqueMetadataMembers(metadataByObjectType.sample), + ['KnownObjectMetadata'], + ), + exportedInterface( + 'CategoryTypedMetadata', + uniqueMetadataMembers(metadataByObjectType.categorySample), + ['KnownSampleMetadata'], + ), + exportedInterface( + 'QuantityTypedMetadata', + uniqueMetadataMembers([...quantityGlobalKeys, ...quantitySpecificKeys]), + ['KnownSampleMetadata'], + ), + exportedInterface( + 'WorkoutTypedMetadata', + uniqueMetadataMembers(metadataByObjectType.workout), + ['KnownSampleMetadata'], + ), + exportedInterface( + 'WorkoutEventTypedMetadata', + uniqueMetadataMembers(metadataByObjectType.workoutEvent), + ), + ) + + const categorySpecificMap = new Map() + for (const key of categorySpecificKeys) { + for (const identifier of key.identifiers) { + const fields = categorySpecificMap.get(identifier) ?? [] + fields.push(key.rawKey) + categorySpecificMap.set(identifier, fields) + } + } + const quantitySpecificMap = new Map() + for (const key of quantitySpecificKeys) { + for (const identifier of key.identifiers) { + const fields = quantitySpecificMap.get(identifier) ?? [] + fields.push(key.rawKey) + quantitySpecificMap.set(identifier, fields) + } + } + + statements.push( + exportedInterface( + 'CategorySpecificMetadataByIdentifierMap', + [...categorySpecificMap.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([identifier, fields]) => + readonlyProperty( + identifier, + createPickType('CategoryTypedMetadata', fields.sort()), + false, + ), + ), + ), + createConditionalGeneratedType( + 'CategoryTypedMetadataForIdentifierGenerated', + 'KnownSampleMetadata', + 'CategorySpecificMetadataByIdentifierMap', + namedType('Record', [ + keywordType(ts.SyntaxKind.StringKeyword), + keywordType(ts.SyntaxKind.NeverKeyword), + ]), + ), + exportedInterface( + 'QuantitySpecificMetadataByIdentifierMap', + [...quantitySpecificMap.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([identifier, fields]) => + readonlyProperty( + identifier, + createPickType('QuantityTypedMetadata', fields.sort()), + false, + ), + ), + ), + exportedTypeAlias( + 'QuantityTypedMetadataForIdentifierGenerated', + FACTORY.createIntersectionTypeNode([ + namedType('KnownSampleMetadata'), + namedType('Pick', [ + namedType('QuantityTypedMetadata'), + createLiteralUnion( + quantityGlobalKeys.map((key) => key.rawKey).sort(), + ), + ]), + FACTORY.createParenthesizedType( + FACTORY.createConditionalTypeNode( + namedType('T'), + createKeyofType('QuantitySpecificMetadataByIdentifierMap'), + FACTORY.createIndexedAccessTypeNode( + namedType('QuantitySpecificMetadataByIdentifierMap'), + namedType('T'), + ), + namedType('Record', [ + keywordType(ts.SyntaxKind.StringKeyword), + keywordType(ts.SyntaxKind.NeverKeyword), + ]), + ), + ), + ]), + [ + FACTORY.createTypeParameterDeclaration( + undefined, + 'T', + namedType('QuantityTypeIdentifier'), + namedType('QuantityTypeIdentifier'), + ), + ], + ), + ) + + statements.push( + exportedInterface( + 'QuantityUnitByIdentifierMap', + schema.quantityIdentifiers.map((identifier) => + readonlyProperty( + identifier.name, + canonicalUnitToTypeNode(identifier.canonicalUnit), + false, + ), + ), + ), + exportedTypeAlias( + 'UnitForIdentifierGenerated', + FACTORY.createConditionalTypeNode( + namedType('T'), + createKeyofType('QuantityUnitByIdentifierMap'), + FACTORY.createIndexedAccessTypeNode( + namedType('QuantityUnitByIdentifierMap'), + namedType('T'), + ), + keywordType(ts.SyntaxKind.StringKeyword), + ), + [ + FACTORY.createTypeParameterDeclaration( + undefined, + 'T', + namedType('QuantityTypeIdentifier'), + namedType('QuantityTypeIdentifier'), + ), + ], + ), + ) + + const sourceFile = ts.factory.createSourceFile( + statements, + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ) + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) + const printed = printer.printFile(sourceFile) + return `/*\n * AUTO-GENERATED FILE. DO NOT EDIT.\n * Source: scripts/generate-healthkit.ts\n */\n\n${printed}` +} + +function renderMetadataValueKindMap( + name: string, + keys: readonly MetadataKeySchema[], +): string { + const entries = unique(keys.map((key) => key.rawKey)) + .sort((left, right) => left.localeCompare(right)) + .map((rawKey) => { + const key = keys.find((entry) => entry.rawKey === rawKey) + if (key == null) { + throw new Error(`Missing metadata key for raw key ${rawKey}`) + } + return ` ${JSON.stringify(rawKey)}: ${JSON.stringify(key.valueKind)},` + }) + .join('\n') + + return `export const ${name} = {\n${entries}\n} as const` +} + +function renderIdentifierMetadataKindMap( + name: string, + keys: readonly MetadataKeySchema[], +): string { + const byIdentifier = new Map() + + for (const key of keys) { + for (const identifier of key.identifiers) { + const existing = byIdentifier.get(identifier) ?? [] + existing.push(key) + byIdentifier.set(identifier, existing) + } + } + + const entries = [...byIdentifier.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([identifier, identifierKeys]) => { + const lines = unique(identifierKeys.map((key) => key.rawKey)) + .sort((left, right) => left.localeCompare(right)) + .map((rawKey) => { + const key = identifierKeys.find((entry) => entry.rawKey === rawKey) + if (key == null) { + throw new Error(`Missing metadata key for raw key ${rawKey}`) + } + return ` ${JSON.stringify(rawKey)}: ${JSON.stringify(key.valueKind)},` + }) + .join('\n') + + return ` ${JSON.stringify(identifier)}: {\n${lines}\n },` + }) + .join('\n') + + return `export const ${name} = {\n${entries}\n} as const` +} + +export function renderGeneratedContracts(schema: HealthkitSchema): string { + const metadataByObjectType = { + common: schema.metadataKeys.filter((key) => + key.objectTypes.includes('common'), + ), + sample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('sample'), + ), + categorySample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('categorySample'), + ), + quantitySample: schema.metadataKeys.filter((key) => + key.objectTypes.includes('quantitySample'), + ), + workout: schema.metadataKeys.filter((key) => + key.objectTypes.includes('workout'), + ), + workoutEvent: schema.metadataKeys.filter((key) => + key.objectTypes.includes('workoutEvent'), + ), + } + + return tsSource`/* + * AUTO-GENERATED FILE. DO NOT EDIT. + * Source: scripts/generate-healthkit.ts + */ + +import { z } from 'zod' +import type { CategoryTypeIdentifier } from '@kingstinct/react-native-healthkit/types/CategoryTypeIdentifier' +import type { QuantityTypeIdentifier } from '@kingstinct/react-native-healthkit/types/QuantityTypeIdentifier' + +export type ContractMetadataValueKind = 'string' | 'boolean' | 'number' | 'quantity' | 'enum' + +${renderMetadataValueKindMap( + 'KNOWN_OBJECT_METADATA_KIND_BY_KEY', + metadataByObjectType.common, +)} + +${renderMetadataValueKindMap( + 'KNOWN_SAMPLE_METADATA_KIND_BY_KEY', + metadataByObjectType.sample, +)} + +${renderMetadataValueKindMap( + 'KNOWN_WORKOUT_METADATA_KIND_BY_KEY', + metadataByObjectType.workout, +)} + +${renderMetadataValueKindMap( + 'KNOWN_WORKOUT_EVENT_METADATA_KIND_BY_KEY', + metadataByObjectType.workoutEvent, +)} + +${renderIdentifierMetadataKindMap( + 'KNOWN_CATEGORY_METADATA_KIND_BY_IDENTIFIER', + metadataByObjectType.categorySample.filter( + (key) => key.identifiers.length > 0, + ), +)} + +${renderIdentifierMetadataKindMap( + 'KNOWN_QUANTITY_METADATA_KIND_BY_IDENTIFIER', + metadataByObjectType.quantitySample.filter( + (key) => key.identifiers.length > 0, + ), +)} + +const CATEGORY_METADATA_KIND_LOOKUP = KNOWN_CATEGORY_METADATA_KIND_BY_IDENTIFIER as Readonly>>> +const QUANTITY_METADATA_KIND_LOOKUP = KNOWN_QUANTITY_METADATA_KIND_BY_IDENTIFIER as Readonly>>> + +export const contractQuantitySchema = z + .object({ + unit: z.string(), + quantity: z.number(), + }) + .passthrough() + +export const contractSourceSchema = z + .object({ + name: z.string(), + bundleIdentifier: z.string(), + }) + .passthrough() + +export const contractSourceRevisionSchema = z + .object({ + source: contractSourceSchema, + operatingSystemVersion: z.string(), + version: z.string().optional(), + productType: z.string().optional(), + }) + .passthrough() + +export const contractDeviceSchema = z + .object({ + name: z.string().optional(), + firmwareVersion: z.string().optional(), + hardwareVersion: z.string().optional(), + localIdentifier: z.string().optional(), + manufacturer: z.string().optional(), + model: z.string().optional(), + softwareVersion: z.string().optional(), + udiDeviceIdentifier: z.string().optional(), + }) + .passthrough() + +export const contractSampleTypeSchema = z + .object({ + identifier: z.string(), + allowsRecalibrationForEstimates: z.boolean(), + isMinimumDurationRestricted: z.boolean(), + isMaximumDurationRestricted: z.boolean(), + }) + .passthrough() + +export const contractWorkoutActivitySchema = z + .object({ + startDate: z.date(), + endDate: z.date(), + uuid: z.string(), + duration: z.number(), + }) + .passthrough() + +function contractSchemaForMetadataKind( + kind: ContractMetadataValueKind, +): z.ZodTypeAny { + switch (kind) { + case 'string': + return z.string() + case 'boolean': + return z.boolean() + case 'number': + case 'enum': + return z.number() + case 'quantity': + return contractQuantitySchema + } +} + +function contractMetadataSchemaFromKinds( + kinds: Readonly>, +) { + const shape: Record = {} + + for (const [key, kind] of Object.entries(kinds)) { + shape[key] = contractSchemaForMetadataKind(kind).optional() + } + + return z.object(shape).passthrough() +} + +function mergeContractMetadataKinds( + ...sources: ReadonlyArray>> +): Record { + return Object.assign({}, ...sources) +} + +export const contractObjectMetadataSchema = contractMetadataSchemaFromKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, +) +export const contractSampleMetadataSchema = contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + ), +) +export const contractWorkoutMetadataSchema = contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + KNOWN_WORKOUT_METADATA_KIND_BY_KEY, + ), +) +export const contractWorkoutEventMetadataSchema = + contractMetadataSchemaFromKinds(KNOWN_WORKOUT_EVENT_METADATA_KIND_BY_KEY) + +const categoryMetadataSchemaLookup = Object.fromEntries( + Object.entries(CATEGORY_METADATA_KIND_LOOKUP).map(([identifier, kinds]) => [ + identifier, + contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + kinds, + ), + ), + ]), +) as Record + +const quantityMetadataSchemaLookup = Object.fromEntries( + Object.entries(QUANTITY_METADATA_KIND_LOOKUP).map(([identifier, kinds]) => [ + identifier, + contractMetadataSchemaFromKinds( + mergeContractMetadataKinds( + KNOWN_OBJECT_METADATA_KIND_BY_KEY, + KNOWN_SAMPLE_METADATA_KIND_BY_KEY, + kinds, + ), + ), + ]), +) as Record + +export function getKnownCategoryMetadataKindMap( + identifier: CategoryTypeIdentifier, +): Readonly> { + return CATEGORY_METADATA_KIND_LOOKUP[identifier] ?? {} +} + +export function getKnownQuantityMetadataKindMap( + identifier: QuantityTypeIdentifier, +): Readonly> { + return QUANTITY_METADATA_KIND_LOOKUP[identifier] ?? {} +} + +export function getCategoryMetadataContractSchema( + identifier: CategoryTypeIdentifier, +): z.ZodTypeAny { + return categoryMetadataSchemaLookup[identifier] ?? contractSampleMetadataSchema +} + +export function getQuantityMetadataContractSchema( + identifier: QuantityTypeIdentifier, +): z.ZodTypeAny { + return quantityMetadataSchemaLookup[identifier] ?? contractSampleMetadataSchema +} + +function createBaseSampleContractSchema(metadataSchema: z.ZodTypeAny) { + return z + .object({ + uuid: z.string(), + sourceRevision: contractSourceRevisionSchema, + device: contractDeviceSchema.optional(), + metadata: metadataSchema, + sampleType: contractSampleTypeSchema, + startDate: z.date(), + endDate: z.date(), + hasUndeterminedDuration: z.boolean(), + }) + .passthrough() +} + +export function getQuantitySampleContractSchema( + identifier: QuantityTypeIdentifier, +) { + return createBaseSampleContractSchema( + getQuantityMetadataContractSchema(identifier), + ) + .extend({ + quantityType: z.literal(identifier), + quantity: z.number(), + unit: z.string(), + }) + .passthrough() +} + +export function getCategorySampleContractSchema( + identifier: CategoryTypeIdentifier, +) { + return createBaseSampleContractSchema( + getCategoryMetadataContractSchema(identifier), + ) + .extend({ + categoryType: z.literal(identifier), + value: z.number(), + }) + .passthrough() +} + +export const contractWorkoutEventSchema = z + .object({ + type: z.number(), + startDate: z.date(), + endDate: z.date(), + metadata: contractWorkoutEventMetadataSchema.optional(), + }) + .passthrough() + +export const contractWorkoutSampleSchema = createBaseSampleContractSchema( + contractWorkoutMetadataSchema, +) + .extend({ + workoutActivityType: z.number(), + duration: contractQuantitySchema, + totalEnergyBurned: contractQuantitySchema.optional(), + totalDistance: contractQuantitySchema.optional(), + totalSwimmingStrokeCount: contractQuantitySchema.optional(), + totalFlightsClimbed: contractQuantitySchema.optional(), + events: z.array(contractWorkoutEventSchema).optional(), + activities: z.array(contractWorkoutActivitySchema).optional(), + }) + .passthrough() +` +} + +export function renderGeneratedSwift(schema: HealthkitSchema): string { + const booleanKeys = unique( + schema.metadataKeys + .filter((key) => key.valueKind === 'boolean') + .map((key) => key.rawKey), + ) + const numericKeys = unique( + schema.metadataKeys + .filter((key) => key.valueKind === 'number' || key.valueKind === 'enum') + .map((key) => key.rawKey), + ) + + const renderSetEntries = (keys: readonly string[]) => + keys.map((key) => ` ${JSON.stringify(key)},`).join('\n') + + return `// AUTO-GENERATED FILE. DO NOT EDIT.\n// Source: scripts/generate-healthkit.ts\n\nimport Foundation\n\nprivate let healthkitBooleanMetadataKeys: Set = [\n${renderSetEntries(booleanKeys)}\n]\n\nprivate let healthkitNumericMetadataKeys: Set = [\n${renderSetEntries(numericKeys)}\n]\n\nfunc isKnownBooleanMetadataKey(_ key: String) -> Bool {\n healthkitBooleanMetadataKeys.contains(key)\n}\n\nfunc isKnownNumericMetadataKey(_ key: String) -> Bool {\n healthkitNumericMetadataKeys.contains(key)\n}\n` +} diff --git a/packages/react-native-healthkit/scripts/healthkit-schema/identifier-overrides.json b/packages/react-native-healthkit/scripts/healthkit-schema/identifier-overrides.json new file mode 100644 index 00000000..d9cb8ef7 --- /dev/null +++ b/packages/react-native-healthkit/scripts/healthkit-schema/identifier-overrides.json @@ -0,0 +1,20 @@ +{ + "quantity": { + "readOnly": [ + "HKQuantityTypeIdentifierWalkingHeartRateAverage", + "HKQuantityTypeIdentifierAtrialFibrillationBurden", + "HKQuantityTypeIdentifierAppleExerciseTime", + "HKQuantityTypeIdentifierAppleStandTime", + "HKQuantityTypeIdentifierAppleWalkingSteadiness" + ] + }, + "category": { + "readOnly": [ + "HKCategoryTypeIdentifierAppleStandHour", + "HKCategoryTypeIdentifierHighHeartRateEvent", + "HKCategoryTypeIdentifierLowHeartRateEvent", + "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent", + "HKCategoryTypeIdentifierHypertensionEvent" + ] + } +} diff --git a/packages/react-native-healthkit/scripts/healthkit-sdk.ts b/packages/react-native-healthkit/scripts/healthkit-sdk.ts new file mode 100644 index 00000000..0e2140bc --- /dev/null +++ b/packages/react-native-healthkit/scripts/healthkit-sdk.ts @@ -0,0 +1,221 @@ +import { execFileSync } from 'node:child_process' +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import type { + HealthkitSchema, + IdentifierOverrides, + SymbolGraphDocument, +} from './generate-healthkit' + +const ROOT = resolve(dirname(new URL(import.meta.url).pathname), '..') +export const DEFAULT_GENERATED_TS_PATH = join( + ROOT, + 'src/generated/healthkit.generated.ts', +) +export const DEFAULT_GENERATED_SCHEMA_PATH = join( + ROOT, + 'src/generated/healthkit-schema.json', +) +export const DEFAULT_GENERATED_CONTRACT_TS_PATH = join( + ROOT, + '..', + '..', + 'apps/example/contracts/generated/healthkit.contract.generated.ts', +) +export const DEFAULT_GENERATED_SWIFT_PATH = join( + ROOT, + 'ios/generated/HealthkitGenerated.swift', +) +const IDENTIFIER_OVERRIDES_PATH = join( + ROOT, + 'scripts/healthkit-schema/identifier-overrides.json', +) + +export interface HealthkitSdkSources { + readonly sdkPath: string + readonly symbolGraph: SymbolGraphDocument + readonly identifierOverrides: IdentifierOverrides + readonly typeIdentifiersHeader: string + readonly categoryValuesHeader: string + readonly metadataHeader: string + readonly metadataEnumsHeader: string + readonly workoutHeader: string +} + +export interface GeneratedArtifactPaths { + readonly typescriptPath: string + readonly schemaPath: string + readonly contractTypescriptPath: string + readonly swiftPath: string +} + +export function getGeneratedArtifactPaths( + overrides: Partial = {}, +): GeneratedArtifactPaths { + return { + typescriptPath: + overrides.typescriptPath ?? + process.env.HEALTHKIT_GENERATED_TS_PATH ?? + DEFAULT_GENERATED_TS_PATH, + schemaPath: + overrides.schemaPath ?? + process.env.HEALTHKIT_GENERATED_SCHEMA_PATH ?? + DEFAULT_GENERATED_SCHEMA_PATH, + contractTypescriptPath: + overrides.contractTypescriptPath ?? + process.env.HEALTHKIT_GENERATED_CONTRACT_TS_PATH ?? + DEFAULT_GENERATED_CONTRACT_TS_PATH, + swiftPath: + overrides.swiftPath ?? + process.env.HEALTHKIT_GENERATED_SWIFT_PATH ?? + DEFAULT_GENERATED_SWIFT_PATH, + } +} + +export function getSdkPath(): string { + if (process.env.HEALTHKIT_SDK_PATH) { + return process.env.HEALTHKIT_SDK_PATH + } + + return execFileSync( + 'xcrun', + ['--sdk', 'iphonesimulator', '--show-sdk-path'], + { + encoding: 'utf8', + }, + ).trim() +} + +export function readIdentifierOverrides(): IdentifierOverrides { + return JSON.parse( + readFileSync(IDENTIFIER_OVERRIDES_PATH, 'utf8'), + ) as IdentifierOverrides +} + +function readHealthkitHeader(relativePath: string, sdkPath: string): string { + return readFileSync( + join( + sdkPath, + 'System/Library/Frameworks/HealthKit.framework', + relativePath, + ), + 'utf8', + ) +} + +function readHealthkitSymbolGraph(sdkPath: string): SymbolGraphDocument { + const outputDir = mkdtempSync(join(tmpdir(), 'healthkit-symbolgraph-')) + + try { + execFileSync( + 'swift', + [ + 'symbolgraph-extract', + '-module-name', + 'HealthKit', + '-target', + 'arm64-apple-ios17.0-simulator', + '-sdk', + sdkPath, + '-output-dir', + outputDir, + ], + { + cwd: ROOT, + }, + ) + + return JSON.parse( + readFileSync(join(outputDir, 'HealthKit.symbols.json'), 'utf8'), + ) as SymbolGraphDocument + } finally { + rmSync(outputDir, { recursive: true, force: true }) + } +} + +export function loadHealthkitSdkSources( + sdkPath = getSdkPath(), +): HealthkitSdkSources { + return { + sdkPath, + symbolGraph: readHealthkitSymbolGraph(sdkPath), + identifierOverrides: readIdentifierOverrides(), + typeIdentifiersHeader: readHealthkitHeader( + 'Headers/HKTypeIdentifiers.h', + sdkPath, + ), + categoryValuesHeader: readHealthkitHeader( + 'Headers/HKCategoryValues.h', + sdkPath, + ), + metadataHeader: readHealthkitHeader('Headers/HKMetadata.h', sdkPath), + metadataEnumsHeader: readHealthkitHeader( + 'Headers/HKMetadataEnums.h', + sdkPath, + ), + workoutHeader: readHealthkitHeader('Headers/HKWorkout.h', sdkPath), + } +} + +export function writeGeneratedArtifacts( + schema: HealthkitSchema, + renderedTypescript: string, + renderedContracts: string, + renderedSwift: string, + paths = getGeneratedArtifactPaths(), +) { + mkdirSync(dirname(paths.typescriptPath), { recursive: true }) + mkdirSync(dirname(paths.swiftPath), { recursive: true }) + mkdirSync(dirname(paths.contractTypescriptPath), { recursive: true }) + + writeFileSync( + paths.schemaPath, + `${JSON.stringify(schema, null, 2)}\n`, + 'utf8', + ) + writeFileSync(paths.typescriptPath, renderedTypescript, 'utf8') + writeFileSync(paths.contractTypescriptPath, renderedContracts, 'utf8') + writeFileSync(paths.swiftPath, renderedSwift, 'utf8') +} + +export function formatGeneratedArtifacts(paths = getGeneratedArtifactPaths()) { + execFileSync( + 'bunx', + [ + '@biomejs/biome', + 'check', + '--write', + paths.typescriptPath, + paths.contractTypescriptPath, + paths.schemaPath, + paths.swiftPath, + ], + { + stdio: 'inherit', + cwd: ROOT, + }, + ) + execFileSync( + 'bunx', + [ + '@biomejs/biome', + 'format', + '--write', + paths.typescriptPath, + paths.contractTypescriptPath, + paths.schemaPath, + paths.swiftPath, + ], + { + stdio: 'inherit', + cwd: ROOT, + }, + ) +} diff --git a/packages/react-native-healthkit/scripts/verify-healthkit-sdk.ts b/packages/react-native-healthkit/scripts/verify-healthkit-sdk.ts new file mode 100644 index 00000000..46f3732b --- /dev/null +++ b/packages/react-native-healthkit/scripts/verify-healthkit-sdk.ts @@ -0,0 +1,140 @@ +import { strict as assert } from 'node:assert' +import { buildHealthkitSchemaFromSources } from './generate-healthkit' +import { loadHealthkitSdkSources } from './healthkit-sdk' + +function findMetadataKey( + schema: ReturnType, + keyConstant: string, +) { + const metadataKey = schema.metadataKeys.find( + (entry) => entry.keyConstant === keyConstant, + ) + + assert.ok(metadataKey, `Missing metadata key ${keyConstant}`) + return metadataKey +} + +function main() { + const sources = loadHealthkitSdkSources() + const schema = buildHealthkitSchemaFromSources(sources) + + const sleepAnalysis = schema.categoryIdentifiers.find( + (identifier) => identifier.name === 'HKCategoryTypeIdentifierSleepAnalysis', + ) + assert.equal( + sleepAnalysis?.valueEnum, + 'HKCategoryValueSleepAnalysis', + 'Sleep analysis should map to HKCategoryValueSleepAnalysis', + ) + + const hypertensionEvent = schema.categoryIdentifiers.find( + (identifier) => + identifier.name === 'HKCategoryTypeIdentifierHypertensionEvent', + ) + assert.ok( + hypertensionEvent, + 'Pinned SDK should expose HKCategoryTypeIdentifierHypertensionEvent', + ) + + const bloodGlucose = schema.quantityIdentifiers.find( + (identifier) => identifier.name === 'HKQuantityTypeIdentifierBloodGlucose', + ) + assert.equal( + bloodGlucose?.canonicalUnit, + 'mg/dL', + 'Blood glucose should retain canonical unit mapping', + ) + + const workoutBrandName = findMetadataKey( + schema, + 'HKMetadataKeyWorkoutBrandName', + ) + assert.equal(workoutBrandName.tsType, 'string') + assert.ok(workoutBrandName.objectTypes.includes('workout')) + + const swimmingStrokeStyle = findMetadataKey( + schema, + 'HKMetadataKeySwimmingStrokeStyle', + ) + assert.equal(swimmingStrokeStyle.enumName, 'SwimmingStrokeStyle') + assert.ok(swimmingStrokeStyle.objectTypes.includes('workoutEvent')) + + const heartRateMotionContext = findMetadataKey( + schema, + 'HKMetadataKeyHeartRateMotionContext', + ) + assert.equal(heartRateMotionContext.enumName, 'HeartRateMotionContext') + assert.ok(heartRateMotionContext.objectTypes.includes('quantitySample')) + + const heartRateEventThreshold = findMetadataKey( + schema, + 'HKMetadataKeyHeartRateEventThreshold', + ) + assert.equal(heartRateEventThreshold.tsType, 'Quantity') + assert.ok( + heartRateEventThreshold.identifiers.includes( + 'HKCategoryTypeIdentifierHighHeartRateEvent', + ), + ) + + const audioExposureLevel = findMetadataKey( + schema, + 'HKMetadataKeyAudioExposureLevel', + ) + assert.equal(audioExposureLevel.tsType, 'Quantity') + assert.ok(audioExposureLevel.objectTypes.includes('categorySample')) + + const appleDeviceCalibrated = findMetadataKey( + schema, + 'HKMetadataKeyAppleDeviceCalibrated', + ) + assert.equal(appleDeviceCalibrated.tsType, 'boolean') + assert.ok(appleDeviceCalibrated.objectTypes.includes('sample')) + + const vo2MaxValue = findMetadataKey(schema, 'HKMetadataKeyVO2MaxValue') + assert.equal(vo2MaxValue.tsType, 'Quantity') + assert.ok( + vo2MaxValue.identifiers.includes( + 'HKCategoryTypeIdentifierLowCardioFitnessEvent', + ), + ) + + const lowCardioFitnessEventThreshold = findMetadataKey( + schema, + 'HKMetadataKeyLowCardioFitnessEventThreshold', + ) + assert.equal(lowCardioFitnessEventThreshold.tsType, 'Quantity') + + const earliestEstimateDate = findMetadataKey( + schema, + 'HKMetadataKeyDateOfEarliestDataUsedForEstimate', + ) + assert.equal(earliestEstimateDate.tsType, 'string') + + const quantityClampedToLowerBound = findMetadataKey( + schema, + 'HKMetadataKeyQuantityClampedToLowerBound', + ) + assert.equal(quantityClampedToLowerBound.tsType, 'boolean') + + const quantityClampedToUpperBound = findMetadataKey( + schema, + 'HKMetadataKeyQuantityClampedToUpperBound', + ) + assert.equal(quantityClampedToUpperBound.tsType, 'boolean') + + const glassesPrescriptionDescription = findMetadataKey( + schema, + 'HKMetadataKeyGlassesPrescriptionDescription', + ) + assert.equal(glassesPrescriptionDescription.tsType, 'string') + + const headphoneGain = findMetadataKey(schema, 'HKMetadataKeyHeadphoneGain') + assert.equal(headphoneGain.tsType, 'Quantity') + + process.stdout.write( + `Verified HealthKit SDK-backed schema invariants using ${sources.sdkPath}\n`, + ) +} + +main() diff --git a/packages/react-native-healthkit/src/generated/healthkit-schema.json b/packages/react-native-healthkit/src/generated/healthkit-schema.json new file mode 100644 index 00000000..c858935e --- /dev/null +++ b/packages/react-native-healthkit/src/generated/healthkit-schema.json @@ -0,0 +1,3680 @@ +{ + "quantityIdentifiers": [ + { + "name": "HKQuantityTypeIdentifierActiveEnergyBurned", + "ios": "8.0", + "canonicalUnit": "kcal", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleExerciseTime", + "ios": "9.3", + "canonicalUnit": "min", + "aggregationStyle": "Cumulative", + "writeable": false, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleMoveTime", + "ios": "14.5", + "canonicalUnit": "min", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances", + "ios": "18.0", + "canonicalUnit": "count", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleSleepingWristTemperature", + "ios": "16.0", + "canonicalUnit": "degC", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleStandTime", + "ios": "13.0", + "canonicalUnit": "min", + "aggregationStyle": "Cumulative", + "writeable": false, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAppleWalkingSteadiness", + "ios": "15.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": false, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierAtrialFibrillationBurden", + "ios": "16.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Temporally Weighted)", + "writeable": false, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBasalBodyTemperature", + "ios": "9.0", + "canonicalUnit": "degC", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBasalEnergyBurned", + "ios": "8.0", + "canonicalUnit": "kcal", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBloodAlcoholContent", + "ios": "8.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBloodGlucose", + "ios": "8.0", + "canonicalUnit": "mg/dL", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBloodPressureDiastolic", + "ios": "8.0", + "canonicalUnit": "mmHg", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBloodPressureSystolic", + "ios": "8.0", + "canonicalUnit": "mmHg", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBodyFatPercentage", + "ios": "8.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBodyMass", + "ios": "8.0", + "canonicalUnit": "kg", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBodyMassIndex", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierBodyTemperature", + "ios": "8.0", + "canonicalUnit": "degC", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierCrossCountrySkiingSpeed", + "ios": "18.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierCyclingCadence", + "ios": "17.0", + "canonicalUnit": "count/min", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierCyclingFunctionalThresholdPower", + "ios": "17.0", + "canonicalUnit": "W", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierCyclingPower", + "ios": "17.0", + "canonicalUnit": "W", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierCyclingSpeed", + "ios": "17.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryBiotin", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryCaffeine", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryCalcium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryCarbohydrates", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryChloride", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryCholesterol", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryChromium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryCopper", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryEnergyConsumed", + "ios": "8.0", + "canonicalUnit": "kcal", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFatMonounsaturated", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFatPolyunsaturated", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFatSaturated", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFatTotal", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFiber", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryFolate", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryIodine", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryIron", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryMagnesium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryManganese", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryMolybdenum", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryNiacin", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryPantothenicAcid", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryPhosphorus", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryPotassium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryProtein", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryRiboflavin", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietarySelenium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietarySodium", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietarySugar", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryThiamin", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminA", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminB12", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminB6", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminC", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminD", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminE", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryVitaminK", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryWater", + "ios": "9.0", + "canonicalUnit": "mL", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDietaryZinc", + "ios": "8.0", + "canonicalUnit": "g", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceCrossCountrySkiing", + "ios": "18.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceCycling", + "ios": "8.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceDownhillSnowSports", + "ios": "11.2", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistancePaddleSports", + "ios": "18.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceRowing", + "ios": "18.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceSkatingSports", + "ios": "18.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceSwimming", + "ios": "10.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceWalkingRunning", + "ios": "8.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierDistanceWheelchair", + "ios": "10.0", + "canonicalUnit": "m", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierElectrodermalActivity", + "ios": "8.0", + "canonicalUnit": "S", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierEnvironmentalAudioExposure", + "ios": "13.0", + "canonicalUnit": "dBASPL", + "aggregationStyle": "Discrete (Equivalent Continuous Level)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierEnvironmentalSoundReduction", + "ios": "16.0", + "canonicalUnit": "dBASPL", + "aggregationStyle": "Discrete (Equivalent Continuous Level)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierEstimatedWorkoutEffortScore", + "ios": "18.0", + "canonicalUnit": "appleEffortScore", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierFlightsClimbed", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierForcedExpiratoryVolume1", + "ios": "8.0", + "canonicalUnit": "L", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierForcedVitalCapacity", + "ios": "8.0", + "canonicalUnit": "L", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierHeadphoneAudioExposure", + "ios": "13.0", + "canonicalUnit": "dBASPL", + "aggregationStyle": "Discrete (Equivalent Continuous Level)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierHeartRate", + "ios": "8.0", + "canonicalUnit": "count/s", + "aggregationStyle": "Discrete (Temporally Weighted)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierHeartRateRecoveryOneMinute", + "ios": "16.0", + "canonicalUnit": "count/min", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierHeartRateVariabilitySDNN", + "ios": "11.0", + "canonicalUnit": "ms", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierHeight", + "ios": "8.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierInhalerUsage", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierInsulinDelivery", + "ios": "11.0", + "canonicalUnit": "IU", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierLeanBodyMass", + "ios": "8.0", + "canonicalUnit": "kg", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierNikeFuel", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierNumberOfAlcoholicBeverages", + "ios": "15.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierNumberOfTimesFallen", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierOxygenSaturation", + "ios": "8.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierPaddleSportsSpeed", + "ios": "18.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierPeakExpiratoryFlowRate", + "ios": "8.0", + "canonicalUnit": "L/min", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierPeripheralPerfusionIndex", + "ios": "8.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierPhysicalEffort", + "ios": "17.0", + "canonicalUnit": "kcal/(kg*hr)", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierPushCount", + "ios": "10.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRespiratoryRate", + "ios": "8.0", + "canonicalUnit": "count/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRestingHeartRate", + "ios": "11.0", + "canonicalUnit": "count/min", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRowingSpeed", + "ios": "18.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRunningGroundContactTime", + "ios": "16.0", + "canonicalUnit": "ms", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRunningPower", + "ios": "16.0", + "canonicalUnit": "W", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRunningSpeed", + "ios": "16.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRunningStrideLength", + "ios": "16.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierRunningVerticalOscillation", + "ios": "16.0", + "canonicalUnit": "cm", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierSixMinuteWalkTestDistance", + "ios": "14.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierStairAscentSpeed", + "ios": "14.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierStairDescentSpeed", + "ios": "14.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierStepCount", + "ios": "8.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierSwimmingStrokeCount", + "ios": "10.0", + "canonicalUnit": "count", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierTimeInDaylight", + "ios": "17.0", + "canonicalUnit": "min", + "aggregationStyle": "Cumulative", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierUnderwaterDepth", + "ios": "16.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierUVExposure", + "ios": "9.0", + "canonicalUnit": null, + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierVO2Max", + "ios": "11.0", + "canonicalUnit": "ml/(kg*min)", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWaistCircumference", + "ios": "11.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWalkingAsymmetryPercentage", + "ios": "14.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage", + "ios": "14.0", + "canonicalUnit": "%", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWalkingHeartRateAverage", + "ios": "11.0", + "canonicalUnit": "count/min", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": false, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWalkingSpeed", + "ios": "14.0", + "canonicalUnit": "m/s", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWalkingStepLength", + "ios": "14.0", + "canonicalUnit": "m", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWaterTemperature", + "ios": "16.0", + "canonicalUnit": "degC", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + }, + { + "name": "HKQuantityTypeIdentifierWorkoutEffortScore", + "ios": "18.0", + "canonicalUnit": "appleEffortScore", + "aggregationStyle": "Discrete (Arithmetic)", + "writeable": true, + "legacy": false + } + ], + "categoryIdentifiers": [ + { + "name": "HKCategoryTypeIdentifierAbdominalCramps", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierAcne", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierAppetiteChanges", + "ios": "13.6", + "valueEnum": "HKCategoryValueAppetiteChanges", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierAppleStandHour", + "ios": "9.0", + "valueEnum": "HKCategoryValueAppleStandHour", + "writeable": false, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierAppleWalkingSteadinessEvent", + "ios": "15.0", + "valueEnum": "HKCategoryValueAppleWalkingSteadinessEvent", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierAudioExposureEvent", + "ios": "13.0", + "valueEnum": null, + "writeable": true, + "legacy": true + }, + { + "name": "HKCategoryTypeIdentifierBladderIncontinence", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierBleedingAfterPregnancy", + "ios": "18.0", + "valueEnum": "HKCategoryValueVaginalBleeding", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierBleedingDuringPregnancy", + "ios": "18.0", + "valueEnum": "HKCategoryValueVaginalBleeding", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierBloating", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierBreastPain", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierCervicalMucusQuality", + "ios": "9.0", + "valueEnum": "HKCategoryValueCervicalMucusQuality", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierChestTightnessOrPain", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierChills", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierConstipation", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierContraceptive", + "ios": "14.3", + "valueEnum": "HKCategoryValueContraceptive", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierCoughing", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierDiarrhea", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierDizziness", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierDrySkin", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent", + "ios": "14.0", + "valueEnum": "HKCategoryValueEnvironmentalAudioExposureEvent", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierFainting", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierFatigue", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierFever", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierGeneralizedBodyAche", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHairLoss", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHandwashingEvent", + "ios": "14.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHeadache", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent", + "ios": "14.2", + "valueEnum": "HKCategoryValueHeadphoneAudioExposureEvent", + "writeable": false, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHeartburn", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHighHeartRateEvent", + "ios": "12.2", + "valueEnum": "HKCategoryValue", + "writeable": false, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHotFlashes", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierHypertensionEvent", + "ios": "26.2", + "valueEnum": "HKCategoryValue", + "writeable": false, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierInfrequentMenstrualCycles", + "ios": "16.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierIntermenstrualBleeding", + "ios": "9.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierIrregularHeartRhythmEvent", + "ios": "12.2", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierIrregularMenstrualCycles", + "ios": "16.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLactation", + "ios": "14.3", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLossOfSmell", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLossOfTaste", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLowCardioFitnessEvent", + "ios": "14.3", + "valueEnum": "HKCategoryValueLowCardioFitnessEvent", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLowerBackPain", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierLowHeartRateEvent", + "ios": "12.2", + "valueEnum": "HKCategoryValue", + "writeable": false, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierMemoryLapse", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierMenstrualFlow", + "ios": "9.0", + "valueEnum": "HKCategoryValueMenstrualFlow", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierMindfulSession", + "ios": "10.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierMoodChanges", + "ios": "13.6", + "valueEnum": "HKCategoryValuePresence", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierNausea", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierNightSweats", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierOvulationTestResult", + "ios": "9.0", + "valueEnum": "HKCategoryValueOvulationTestResult", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierPelvicPain", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierPersistentIntermenstrualBleeding", + "ios": "16.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierPregnancy", + "ios": "14.3", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierPregnancyTestResult", + "ios": "15.0", + "valueEnum": "HKCategoryValuePregnancyTestResult", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierProgesteroneTestResult", + "ios": "15.0", + "valueEnum": "HKCategoryValueProgesteroneTestResult", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierProlongedMenstrualPeriods", + "ios": "16.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierRunnyNose", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSexualActivity", + "ios": "9.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierShortnessOfBreath", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSinusCongestion", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSkippedHeartbeat", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSleepAnalysis", + "ios": "8.0", + "valueEnum": "HKCategoryValueSleepAnalysis", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSleepApneaEvent", + "ios": "18.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSleepChanges", + "ios": "13.6", + "valueEnum": "HKCategoryValuePresence", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierSoreThroat", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierToothbrushingEvent", + "ios": "13.0", + "valueEnum": "HKCategoryValue", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierVaginalDryness", + "ios": "14.0", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierVomiting", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + }, + { + "name": "HKCategoryTypeIdentifierWheezing", + "ios": "13.6", + "valueEnum": "HKCategoryValueSeverity", + "writeable": true, + "legacy": false + } + ], + "enums": [ + { + "name": "AppleECGAlgorithmVersion", + "ios": "14.0", + "members": [ + { + "name": "value1", + "swiftName": "HKAppleECGAlgorithmVersion1", + "rawValue": 1 + }, + { + "name": "value2", + "swiftName": "HKAppleECGAlgorithmVersion2", + "rawValue": 2 + } + ] + }, + { + "name": "BloodGlucoseMealTime", + "ios": "11.0", + "members": [ + { + "name": "preprandial", + "swiftName": "HKBloodGlucoseMealTimePreprandial", + "rawValue": 1 + }, + { + "name": "postprandial", + "swiftName": "HKBloodGlucoseMealTimePostprandial", + "rawValue": 2 + } + ] + }, + { + "name": "BodyTemperatureSensorLocation", + "ios": "8.0", + "members": [ + { + "name": "other", + "swiftName": "HKBodyTemperatureSensorLocationOther", + "rawValue": 0 + }, + { + "name": "armpit", + "swiftName": "HKBodyTemperatureSensorLocationArmpit", + "rawValue": 1 + }, + { + "name": "body", + "swiftName": "HKBodyTemperatureSensorLocationBody", + "rawValue": 2 + }, + { + "name": "ear", + "swiftName": "HKBodyTemperatureSensorLocationEar", + "rawValue": 3 + }, + { + "name": "finger", + "swiftName": "HKBodyTemperatureSensorLocationFinger", + "rawValue": 4 + }, + { + "name": "gastroIntestinal", + "swiftName": "HKBodyTemperatureSensorLocationGastroIntestinal", + "rawValue": 5 + }, + { + "name": "mouth", + "swiftName": "HKBodyTemperatureSensorLocationMouth", + "rawValue": 6 + }, + { + "name": "rectum", + "swiftName": "HKBodyTemperatureSensorLocationRectum", + "rawValue": 7 + }, + { + "name": "toe", + "swiftName": "HKBodyTemperatureSensorLocationToe", + "rawValue": 8 + }, + { + "name": "earDrum", + "swiftName": "HKBodyTemperatureSensorLocationEarDrum", + "rawValue": 9 + }, + { + "name": "temporalArtery", + "swiftName": "HKBodyTemperatureSensorLocationTemporalArtery", + "rawValue": 10 + }, + { + "name": "forehead", + "swiftName": "HKBodyTemperatureSensorLocationForehead", + "rawValue": 11 + } + ] + }, + { + "name": "CategoryValue", + "ios": "9.0", + "members": [ + { + "name": "notApplicable", + "swiftName": "HKCategoryValueNotApplicable", + "rawValue": 0 + } + ] + }, + { + "name": "CategoryValueAppetiteChanges", + "ios": "13.6", + "members": [ + { + "name": "unspecified", + "swiftName": "HKCategoryValueAppetiteChangesUnspecified", + "rawValue": 0 + }, + { + "name": "noChange", + "swiftName": "HKCategoryValueAppetiteChangesNoChange", + "rawValue": 1 + }, + { + "name": "decreased", + "swiftName": "HKCategoryValueAppetiteChangesDecreased", + "rawValue": 2 + }, + { + "name": "increased", + "swiftName": "HKCategoryValueAppetiteChangesIncreased", + "rawValue": 3 + } + ] + }, + { + "name": "CategoryValueAppleStandHour", + "ios": "9.0", + "members": [ + { + "name": "stood", + "swiftName": "HKCategoryValueAppleStandHourStood", + "rawValue": 0 + }, + { + "name": "idle", + "swiftName": "HKCategoryValueAppleStandHourIdle", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueAppleWalkingSteadinessEvent", + "ios": "15.0", + "members": [ + { + "name": "initialLow", + "swiftName": "HKCategoryValueAppleWalkingSteadinessEventInitialLow", + "rawValue": 1 + }, + { + "name": "initialVeryLow", + "swiftName": "HKCategoryValueAppleWalkingSteadinessEventInitialVeryLow", + "rawValue": 2 + }, + { + "name": "repeatLow", + "swiftName": "HKCategoryValueAppleWalkingSteadinessEventRepeatLow", + "rawValue": 3 + }, + { + "name": "repeatVeryLow", + "swiftName": "HKCategoryValueAppleWalkingSteadinessEventRepeatVeryLow", + "rawValue": 4 + } + ] + }, + { + "name": "CategoryValueAudioExposureEvent", + "ios": "13.0", + "members": [ + { + "name": "loudEnvironment", + "swiftName": "HKCategoryValueAudioExposureEventLoudEnvironment", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueCervicalMucusQuality", + "ios": "9.0", + "members": [ + { + "name": "dry", + "swiftName": "HKCategoryValueCervicalMucusQualityDry", + "rawValue": 1 + }, + { + "name": "sticky", + "swiftName": "HKCategoryValueCervicalMucusQualitySticky", + "rawValue": 2 + }, + { + "name": "creamy", + "swiftName": "HKCategoryValueCervicalMucusQualityCreamy", + "rawValue": 3 + }, + { + "name": "watery", + "swiftName": "HKCategoryValueCervicalMucusQualityWatery", + "rawValue": 4 + }, + { + "name": "eggWhite", + "swiftName": "HKCategoryValueCervicalMucusQualityEggWhite", + "rawValue": 5 + } + ] + }, + { + "name": "CategoryValueContraceptive", + "ios": "14.3", + "members": [ + { + "name": "unspecified", + "swiftName": "HKCategoryValueContraceptiveUnspecified", + "rawValue": 1 + }, + { + "name": "implant", + "swiftName": "HKCategoryValueContraceptiveImplant", + "rawValue": 2 + }, + { + "name": "injection", + "swiftName": "HKCategoryValueContraceptiveInjection", + "rawValue": 3 + }, + { + "name": "intrauterineDevice", + "swiftName": "HKCategoryValueContraceptiveIntrauterineDevice", + "rawValue": 4 + }, + { + "name": "intravaginalRing", + "swiftName": "HKCategoryValueContraceptiveIntravaginalRing", + "rawValue": 5 + }, + { + "name": "oral", + "swiftName": "HKCategoryValueContraceptiveOral", + "rawValue": 6 + }, + { + "name": "patch", + "swiftName": "HKCategoryValueContraceptivePatch", + "rawValue": 7 + } + ] + }, + { + "name": "CategoryValueEnvironmentalAudioExposureEvent", + "ios": "14.0", + "members": [ + { + "name": "momentaryLimit", + "swiftName": "HKCategoryValueEnvironmentalAudioExposureEventMomentaryLimit", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueHeadphoneAudioExposureEvent", + "ios": "14.2", + "members": [ + { + "name": "sevenDayLimit", + "swiftName": "HKCategoryValueHeadphoneAudioExposureEventSevenDayLimit", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueLowCardioFitnessEvent", + "ios": "14.3", + "members": [ + { + "name": "lowFitness", + "swiftName": "HKCategoryValueLowCardioFitnessEventLowFitness", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueMenstrualFlow", + "ios": "9.0", + "members": [ + { + "name": "unspecified", + "swiftName": "HKCategoryValueMenstrualFlowUnspecified", + "rawValue": 1 + }, + { + "name": "light", + "swiftName": "HKCategoryValueMenstrualFlowLight", + "rawValue": 2 + }, + { + "name": "medium", + "swiftName": "HKCategoryValueMenstrualFlowMedium", + "rawValue": 3 + }, + { + "name": "heavy", + "swiftName": "HKCategoryValueMenstrualFlowHeavy", + "rawValue": 4 + }, + { + "name": "none", + "swiftName": "HKCategoryValueMenstrualFlowNone", + "rawValue": 5 + } + ] + }, + { + "name": "CategoryValueOvulationTestResult", + "ios": "9.0", + "members": [ + { + "name": "negative", + "swiftName": "HKCategoryValueOvulationTestResultNegative", + "rawValue": 1 + }, + { + "name": "luteinizingHormoneSurge", + "swiftName": "HKCategoryValueOvulationTestResultLuteinizingHormoneSurge", + "rawValue": 2 + }, + { + "name": "positive", + "swiftName": "HKCategoryValueOvulationTestResultPositive", + "rawValue": 2 + }, + { + "name": "indeterminate", + "swiftName": "HKCategoryValueOvulationTestResultIndeterminate", + "rawValue": 3 + }, + { + "name": "estrogenSurge", + "swiftName": "HKCategoryValueOvulationTestResultEstrogenSurge", + "rawValue": 4 + } + ] + }, + { + "name": "CategoryValuePregnancyTestResult", + "ios": "15.0", + "members": [ + { + "name": "negative", + "swiftName": "HKCategoryValuePregnancyTestResultNegative", + "rawValue": 1 + }, + { + "name": "positive", + "swiftName": "HKCategoryValuePregnancyTestResultPositive", + "rawValue": 2 + }, + { + "name": "indeterminate", + "swiftName": "HKCategoryValuePregnancyTestResultIndeterminate", + "rawValue": 3 + } + ] + }, + { + "name": "CategoryValuePresence", + "ios": "13.6", + "members": [ + { + "name": "present", + "swiftName": "HKCategoryValuePresencePresent", + "rawValue": 0 + }, + { + "name": "notPresent", + "swiftName": "HKCategoryValuePresenceNotPresent", + "rawValue": 1 + } + ] + }, + { + "name": "CategoryValueProgesteroneTestResult", + "ios": "15.0", + "members": [ + { + "name": "negative", + "swiftName": "HKCategoryValueProgesteroneTestResultNegative", + "rawValue": 1 + }, + { + "name": "positive", + "swiftName": "HKCategoryValueProgesteroneTestResultPositive", + "rawValue": 2 + }, + { + "name": "indeterminate", + "swiftName": "HKCategoryValueProgesteroneTestResultIndeterminate", + "rawValue": 3 + } + ] + }, + { + "name": "CategoryValueSeverity", + "ios": "13.6", + "members": [ + { + "name": "unspecified", + "swiftName": "HKCategoryValueSeverityUnspecified", + "rawValue": 0 + }, + { + "name": "notPresent", + "swiftName": "HKCategoryValueSeverityNotPresent", + "rawValue": 1 + }, + { + "name": "mild", + "swiftName": "HKCategoryValueSeverityMild", + "rawValue": 2 + }, + { + "name": "moderate", + "swiftName": "HKCategoryValueSeverityModerate", + "rawValue": 3 + }, + { + "name": "severe", + "swiftName": "HKCategoryValueSeveritySevere", + "rawValue": 4 + } + ] + }, + { + "name": "CategoryValueSleepAnalysis", + "ios": "8.0", + "members": [ + { + "name": "inBed", + "swiftName": "HKCategoryValueSleepAnalysisInBed", + "rawValue": 0 + }, + { + "name": "asleepUnspecified", + "swiftName": "HKCategoryValueSleepAnalysisAsleepUnspecified", + "rawValue": 1 + }, + { + "name": "asleep", + "swiftName": "HKCategoryValueSleepAnalysisAsleep", + "rawValue": 1 + }, + { + "name": "awake", + "swiftName": "HKCategoryValueSleepAnalysisAwake", + "rawValue": 2 + }, + { + "name": "asleepCore", + "swiftName": "HKCategoryValueSleepAnalysisAsleepCore", + "rawValue": 3 + }, + { + "name": "asleepDeep", + "swiftName": "HKCategoryValueSleepAnalysisAsleepDeep", + "rawValue": 4 + }, + { + "name": "asleepREM", + "swiftName": "HKCategoryValueSleepAnalysisAsleepREM", + "rawValue": 5 + } + ] + }, + { + "name": "CategoryValueVaginalBleeding", + "ios": "18.0", + "members": [ + { + "name": "unspecified", + "swiftName": "HKCategoryValueVaginalBleedingUnspecified", + "rawValue": 1 + }, + { + "name": "light", + "swiftName": "HKCategoryValueVaginalBleedingLight", + "rawValue": 2 + }, + { + "name": "medium", + "swiftName": "HKCategoryValueVaginalBleedingMedium", + "rawValue": 3 + }, + { + "name": "heavy", + "swiftName": "HKCategoryValueVaginalBleedingHeavy", + "rawValue": 4 + }, + { + "name": "none", + "swiftName": "HKCategoryValueVaginalBleedingNone", + "rawValue": 5 + } + ] + }, + { + "name": "CyclingFunctionalThresholdPowerTestType", + "ios": "17.0", + "members": [ + { + "name": "maxExercise60Minute", + "swiftName": "HKCyclingFunctionalThresholdPowerTestTypeMaxExercise60Minute", + "rawValue": 1 + }, + { + "name": "maxExercise20Minute", + "swiftName": "HKCyclingFunctionalThresholdPowerTestTypeMaxExercise20Minute", + "rawValue": 2 + }, + { + "name": "rampTest", + "swiftName": "HKCyclingFunctionalThresholdPowerTestTypeRampTest", + "rawValue": 3 + }, + { + "name": "predictionExercise", + "swiftName": "HKCyclingFunctionalThresholdPowerTestTypePredictionExercise", + "rawValue": 4 + } + ] + }, + { + "name": "DevicePlacementSide", + "ios": "14.0", + "members": [ + { + "name": "unknown", + "swiftName": "HKDevicePlacementSideUnknown", + "rawValue": 0 + }, + { + "name": "left", + "swiftName": "HKDevicePlacementSideLeft", + "rawValue": 1 + }, + { + "name": "right", + "swiftName": "HKDevicePlacementSideRight", + "rawValue": 2 + }, + { + "name": "central", + "swiftName": "HKDevicePlacementSideCentral", + "rawValue": 3 + } + ] + }, + { + "name": "HeartRateMotionContext", + "ios": "11.0", + "members": [ + { + "name": "notSet", + "swiftName": "HKHeartRateMotionContextNotSet", + "rawValue": 0 + }, + { + "name": "sedentary", + "swiftName": "HKHeartRateMotionContextSedentary", + "rawValue": 1 + }, + { + "name": "active", + "swiftName": "HKHeartRateMotionContextActive", + "rawValue": 2 + } + ] + }, + { + "name": "HeartRateRecoveryTestType", + "ios": "16.0", + "members": [ + { + "name": "maxExercise", + "swiftName": "HKHeartRateRecoveryTestTypeMaxExercise", + "rawValue": 1 + }, + { + "name": "predictionSubMaxExercise", + "swiftName": "HKHeartRateRecoveryTestTypePredictionSubMaxExercise", + "rawValue": 2 + }, + { + "name": "predictionNonExercise", + "swiftName": "HKHeartRateRecoveryTestTypePredictionNonExercise", + "rawValue": 3 + } + ] + }, + { + "name": "HeartRateSensorLocation", + "ios": "8.0", + "members": [ + { + "name": "other", + "swiftName": "HKHeartRateSensorLocationOther", + "rawValue": 0 + }, + { + "name": "chest", + "swiftName": "HKHeartRateSensorLocationChest", + "rawValue": 1 + }, + { + "name": "wrist", + "swiftName": "HKHeartRateSensorLocationWrist", + "rawValue": 2 + }, + { + "name": "finger", + "swiftName": "HKHeartRateSensorLocationFinger", + "rawValue": 3 + }, + { + "name": "hand", + "swiftName": "HKHeartRateSensorLocationHand", + "rawValue": 4 + }, + { + "name": "earLobe", + "swiftName": "HKHeartRateSensorLocationEarLobe", + "rawValue": 5 + }, + { + "name": "foot", + "swiftName": "HKHeartRateSensorLocationFoot", + "rawValue": 6 + } + ] + }, + { + "name": "InsulinDeliveryReason", + "ios": "11.0", + "members": [ + { + "name": "basal", + "swiftName": "HKInsulinDeliveryReasonBasal", + "rawValue": 1 + }, + { + "name": "bolus", + "swiftName": "HKInsulinDeliveryReasonBolus", + "rawValue": 2 + } + ] + }, + { + "name": "PhysicalEffortEstimationType", + "ios": "17.0", + "members": [ + { + "name": "activityLookup", + "swiftName": "HKPhysicalEffortEstimationTypeActivityLookup", + "rawValue": 1 + }, + { + "name": "deviceSensed", + "swiftName": "HKPhysicalEffortEstimationTypeDeviceSensed", + "rawValue": 2 + } + ] + }, + { + "name": "SwimmingStrokeStyle", + "ios": "10.0", + "members": [ + { + "name": "unknown", + "swiftName": "HKSwimmingStrokeStyleUnknown", + "rawValue": 0 + }, + { + "name": "mixed", + "swiftName": "HKSwimmingStrokeStyleMixed", + "rawValue": 1 + }, + { + "name": "freestyle", + "swiftName": "HKSwimmingStrokeStyleFreestyle", + "rawValue": 2 + }, + { + "name": "backstroke", + "swiftName": "HKSwimmingStrokeStyleBackstroke", + "rawValue": 3 + }, + { + "name": "breaststroke", + "swiftName": "HKSwimmingStrokeStyleBreaststroke", + "rawValue": 4 + }, + { + "name": "butterfly", + "swiftName": "HKSwimmingStrokeStyleButterfly", + "rawValue": 5 + }, + { + "name": "kickboard", + "swiftName": "HKSwimmingStrokeStyleKickboard", + "rawValue": 6 + } + ] + }, + { + "name": "UserMotionContext", + "ios": "16.0", + "members": [ + { + "name": "notSet", + "swiftName": "HKUserMotionContextNotSet", + "rawValue": 0 + }, + { + "name": "stationary", + "swiftName": "HKUserMotionContextStationary", + "rawValue": 1 + }, + { + "name": "active", + "swiftName": "HKUserMotionContextActive", + "rawValue": 2 + } + ] + }, + { + "name": "VO2MaxTestType", + "ios": "11.0", + "members": [ + { + "name": "maxExercise", + "swiftName": "HKVO2MaxTestTypeMaxExercise", + "rawValue": 1 + }, + { + "name": "predictionSubMaxExercise", + "swiftName": "HKVO2MaxTestTypePredictionSubMaxExercise", + "rawValue": 2 + }, + { + "name": "predictionNonExercise", + "swiftName": "HKVO2MaxTestTypePredictionNonExercise", + "rawValue": 3 + }, + { + "name": "predictionStepTest", + "swiftName": "HKVO2MaxTestTypePredictionStepTest", + "rawValue": 4 + } + ] + }, + { + "name": "WaterSalinity", + "ios": "17.0", + "members": [ + { + "name": "freshWater", + "swiftName": "HKWaterSalinityFreshWater", + "rawValue": 1 + }, + { + "name": "saltWater", + "swiftName": "HKWaterSalinitySaltWater", + "rawValue": 2 + } + ] + }, + { + "name": "WeatherCondition", + "ios": "10.0", + "members": [ + { + "name": "none", + "swiftName": "HKWeatherConditionNone", + "rawValue": 0 + }, + { + "name": "clear", + "swiftName": "HKWeatherConditionClear", + "rawValue": 1 + }, + { + "name": "fair", + "swiftName": "HKWeatherConditionFair", + "rawValue": 2 + }, + { + "name": "partlyCloudy", + "swiftName": "HKWeatherConditionPartlyCloudy", + "rawValue": 3 + }, + { + "name": "mostlyCloudy", + "swiftName": "HKWeatherConditionMostlyCloudy", + "rawValue": 4 + }, + { + "name": "cloudy", + "swiftName": "HKWeatherConditionCloudy", + "rawValue": 5 + }, + { + "name": "foggy", + "swiftName": "HKWeatherConditionFoggy", + "rawValue": 6 + }, + { + "name": "haze", + "swiftName": "HKWeatherConditionHaze", + "rawValue": 7 + }, + { + "name": "windy", + "swiftName": "HKWeatherConditionWindy", + "rawValue": 8 + }, + { + "name": "blustery", + "swiftName": "HKWeatherConditionBlustery", + "rawValue": 9 + }, + { + "name": "smoky", + "swiftName": "HKWeatherConditionSmoky", + "rawValue": 10 + }, + { + "name": "dust", + "swiftName": "HKWeatherConditionDust", + "rawValue": 11 + }, + { + "name": "snow", + "swiftName": "HKWeatherConditionSnow", + "rawValue": 12 + }, + { + "name": "hail", + "swiftName": "HKWeatherConditionHail", + "rawValue": 13 + }, + { + "name": "sleet", + "swiftName": "HKWeatherConditionSleet", + "rawValue": 14 + }, + { + "name": "freezingDrizzle", + "swiftName": "HKWeatherConditionFreezingDrizzle", + "rawValue": 15 + }, + { + "name": "freezingRain", + "swiftName": "HKWeatherConditionFreezingRain", + "rawValue": 16 + }, + { + "name": "mixedRainAndHail", + "swiftName": "HKWeatherConditionMixedRainAndHail", + "rawValue": 17 + }, + { + "name": "mixedRainAndSnow", + "swiftName": "HKWeatherConditionMixedRainAndSnow", + "rawValue": 18 + }, + { + "name": "mixedRainAndSleet", + "swiftName": "HKWeatherConditionMixedRainAndSleet", + "rawValue": 19 + }, + { + "name": "mixedSnowAndSleet", + "swiftName": "HKWeatherConditionMixedSnowAndSleet", + "rawValue": 20 + }, + { + "name": "drizzle", + "swiftName": "HKWeatherConditionDrizzle", + "rawValue": 21 + }, + { + "name": "scatteredShowers", + "swiftName": "HKWeatherConditionScatteredShowers", + "rawValue": 22 + }, + { + "name": "showers", + "swiftName": "HKWeatherConditionShowers", + "rawValue": 23 + }, + { + "name": "thunderstorms", + "swiftName": "HKWeatherConditionThunderstorms", + "rawValue": 24 + }, + { + "name": "tropicalStorm", + "swiftName": "HKWeatherConditionTropicalStorm", + "rawValue": 25 + }, + { + "name": "hurricane", + "swiftName": "HKWeatherConditionHurricane", + "rawValue": 26 + }, + { + "name": "tornado", + "swiftName": "HKWeatherConditionTornado", + "rawValue": 27 + } + ] + }, + { + "name": "WorkoutActivityType", + "ios": "8.0", + "members": [ + { + "name": "americanFootball", + "swiftName": "HKWorkoutActivityTypeAmericanFootball", + "rawValue": 1 + }, + { + "name": "archery", + "swiftName": "HKWorkoutActivityTypeArchery", + "rawValue": 2 + }, + { + "name": "australianFootball", + "swiftName": "HKWorkoutActivityTypeAustralianFootball", + "rawValue": 3 + }, + { + "name": "badminton", + "swiftName": "HKWorkoutActivityTypeBadminton", + "rawValue": 4 + }, + { + "name": "baseball", + "swiftName": "HKWorkoutActivityTypeBaseball", + "rawValue": 5 + }, + { + "name": "basketball", + "swiftName": "HKWorkoutActivityTypeBasketball", + "rawValue": 6 + }, + { + "name": "bowling", + "swiftName": "HKWorkoutActivityTypeBowling", + "rawValue": 7 + }, + { + "name": "boxing", + "swiftName": "HKWorkoutActivityTypeBoxing", + "rawValue": 8 + }, + { + "name": "climbing", + "swiftName": "HKWorkoutActivityTypeClimbing", + "rawValue": 9 + }, + { + "name": "cricket", + "swiftName": "HKWorkoutActivityTypeCricket", + "rawValue": 10 + }, + { + "name": "crossTraining", + "swiftName": "HKWorkoutActivityTypeCrossTraining", + "rawValue": 11 + }, + { + "name": "curling", + "swiftName": "HKWorkoutActivityTypeCurling", + "rawValue": 12 + }, + { + "name": "cycling", + "swiftName": "HKWorkoutActivityTypeCycling", + "rawValue": 13 + }, + { + "name": "dance", + "swiftName": "HKWorkoutActivityTypeDance", + "rawValue": 14 + }, + { + "name": "danceInspiredTraining", + "swiftName": "HKWorkoutActivityTypeDanceInspiredTraining", + "rawValue": 15 + }, + { + "name": "elliptical", + "swiftName": "HKWorkoutActivityTypeElliptical", + "rawValue": 16 + }, + { + "name": "equestrianSports", + "swiftName": "HKWorkoutActivityTypeEquestrianSports", + "rawValue": 17 + }, + { + "name": "fencing", + "swiftName": "HKWorkoutActivityTypeFencing", + "rawValue": 18 + }, + { + "name": "fishing", + "swiftName": "HKWorkoutActivityTypeFishing", + "rawValue": 19 + }, + { + "name": "functionalStrengthTraining", + "swiftName": "HKWorkoutActivityTypeFunctionalStrengthTraining", + "rawValue": 20 + }, + { + "name": "golf", + "swiftName": "HKWorkoutActivityTypeGolf", + "rawValue": 21 + }, + { + "name": "gymnastics", + "swiftName": "HKWorkoutActivityTypeGymnastics", + "rawValue": 22 + }, + { + "name": "handball", + "swiftName": "HKWorkoutActivityTypeHandball", + "rawValue": 23 + }, + { + "name": "hiking", + "swiftName": "HKWorkoutActivityTypeHiking", + "rawValue": 24 + }, + { + "name": "hockey", + "swiftName": "HKWorkoutActivityTypeHockey", + "rawValue": 25 + }, + { + "name": "hunting", + "swiftName": "HKWorkoutActivityTypeHunting", + "rawValue": 26 + }, + { + "name": "lacrosse", + "swiftName": "HKWorkoutActivityTypeLacrosse", + "rawValue": 27 + }, + { + "name": "martialArts", + "swiftName": "HKWorkoutActivityTypeMartialArts", + "rawValue": 28 + }, + { + "name": "mindAndBody", + "swiftName": "HKWorkoutActivityTypeMindAndBody", + "rawValue": 29 + }, + { + "name": "mixedMetabolicCardioTraining", + "swiftName": "HKWorkoutActivityTypeMixedMetabolicCardioTraining", + "rawValue": 30 + }, + { + "name": "paddleSports", + "swiftName": "HKWorkoutActivityTypePaddleSports", + "rawValue": 31 + }, + { + "name": "play", + "swiftName": "HKWorkoutActivityTypePlay", + "rawValue": 32 + }, + { + "name": "preparationAndRecovery", + "swiftName": "HKWorkoutActivityTypePreparationAndRecovery", + "rawValue": 33 + }, + { + "name": "racquetball", + "swiftName": "HKWorkoutActivityTypeRacquetball", + "rawValue": 34 + }, + { + "name": "rowing", + "swiftName": "HKWorkoutActivityTypeRowing", + "rawValue": 35 + }, + { + "name": "rugby", + "swiftName": "HKWorkoutActivityTypeRugby", + "rawValue": 36 + }, + { + "name": "running", + "swiftName": "HKWorkoutActivityTypeRunning", + "rawValue": 37 + }, + { + "name": "sailing", + "swiftName": "HKWorkoutActivityTypeSailing", + "rawValue": 38 + }, + { + "name": "skatingSports", + "swiftName": "HKWorkoutActivityTypeSkatingSports", + "rawValue": 39 + }, + { + "name": "snowSports", + "swiftName": "HKWorkoutActivityTypeSnowSports", + "rawValue": 40 + }, + { + "name": "soccer", + "swiftName": "HKWorkoutActivityTypeSoccer", + "rawValue": 41 + }, + { + "name": "softball", + "swiftName": "HKWorkoutActivityTypeSoftball", + "rawValue": 42 + }, + { + "name": "squash", + "swiftName": "HKWorkoutActivityTypeSquash", + "rawValue": 43 + }, + { + "name": "stairClimbing", + "swiftName": "HKWorkoutActivityTypeStairClimbing", + "rawValue": 44 + }, + { + "name": "surfingSports", + "swiftName": "HKWorkoutActivityTypeSurfingSports", + "rawValue": 45 + }, + { + "name": "swimming", + "swiftName": "HKWorkoutActivityTypeSwimming", + "rawValue": 46 + }, + { + "name": "tableTennis", + "swiftName": "HKWorkoutActivityTypeTableTennis", + "rawValue": 47 + }, + { + "name": "tennis", + "swiftName": "HKWorkoutActivityTypeTennis", + "rawValue": 48 + }, + { + "name": "trackAndField", + "swiftName": "HKWorkoutActivityTypeTrackAndField", + "rawValue": 49 + }, + { + "name": "traditionalStrengthTraining", + "swiftName": "HKWorkoutActivityTypeTraditionalStrengthTraining", + "rawValue": 50 + }, + { + "name": "volleyball", + "swiftName": "HKWorkoutActivityTypeVolleyball", + "rawValue": 51 + }, + { + "name": "walking", + "swiftName": "HKWorkoutActivityTypeWalking", + "rawValue": 52 + }, + { + "name": "waterFitness", + "swiftName": "HKWorkoutActivityTypeWaterFitness", + "rawValue": 53 + }, + { + "name": "waterPolo", + "swiftName": "HKWorkoutActivityTypeWaterPolo", + "rawValue": 54 + }, + { + "name": "waterSports", + "swiftName": "HKWorkoutActivityTypeWaterSports", + "rawValue": 55 + }, + { + "name": "wrestling", + "swiftName": "HKWorkoutActivityTypeWrestling", + "rawValue": 56 + }, + { + "name": "yoga", + "swiftName": "HKWorkoutActivityTypeYoga", + "rawValue": 57 + }, + { + "name": "barre", + "swiftName": "HKWorkoutActivityTypeBarre", + "rawValue": 58 + }, + { + "name": "coreTraining", + "swiftName": "HKWorkoutActivityTypeCoreTraining", + "rawValue": 59 + }, + { + "name": "crossCountrySkiing", + "swiftName": "HKWorkoutActivityTypeCrossCountrySkiing", + "rawValue": 60 + }, + { + "name": "downhillSkiing", + "swiftName": "HKWorkoutActivityTypeDownhillSkiing", + "rawValue": 61 + }, + { + "name": "flexibility", + "swiftName": "HKWorkoutActivityTypeFlexibility", + "rawValue": 62 + }, + { + "name": "highIntensityIntervalTraining", + "swiftName": "HKWorkoutActivityTypeHighIntensityIntervalTraining", + "rawValue": 63 + }, + { + "name": "jumpRope", + "swiftName": "HKWorkoutActivityTypeJumpRope", + "rawValue": 64 + }, + { + "name": "kickboxing", + "swiftName": "HKWorkoutActivityTypeKickboxing", + "rawValue": 65 + }, + { + "name": "pilates", + "swiftName": "HKWorkoutActivityTypePilates", + "rawValue": 66 + }, + { + "name": "snowboarding", + "swiftName": "HKWorkoutActivityTypeSnowboarding", + "rawValue": 67 + }, + { + "name": "stairs", + "swiftName": "HKWorkoutActivityTypeStairs", + "rawValue": 68 + }, + { + "name": "stepTraining", + "swiftName": "HKWorkoutActivityTypeStepTraining", + "rawValue": 69 + }, + { + "name": "wheelchairWalkPace", + "swiftName": "HKWorkoutActivityTypeWheelchairWalkPace", + "rawValue": 70 + }, + { + "name": "wheelchairRunPace", + "swiftName": "HKWorkoutActivityTypeWheelchairRunPace", + "rawValue": 71 + }, + { + "name": "taiChi", + "swiftName": "HKWorkoutActivityTypeTaiChi", + "rawValue": 72 + }, + { + "name": "mixedCardio", + "swiftName": "HKWorkoutActivityTypeMixedCardio", + "rawValue": 73 + }, + { + "name": "handCycling", + "swiftName": "HKWorkoutActivityTypeHandCycling", + "rawValue": 74 + }, + { + "name": "discSports", + "swiftName": "HKWorkoutActivityTypeDiscSports", + "rawValue": 75 + }, + { + "name": "fitnessGaming", + "swiftName": "HKWorkoutActivityTypeFitnessGaming", + "rawValue": 76 + }, + { + "name": "cardioDance", + "swiftName": "HKWorkoutActivityTypeCardioDance", + "rawValue": 77 + }, + { + "name": "socialDance", + "swiftName": "HKWorkoutActivityTypeSocialDance", + "rawValue": 78 + }, + { + "name": "pickleball", + "swiftName": "HKWorkoutActivityTypePickleball", + "rawValue": 79 + }, + { + "name": "cooldown", + "swiftName": "HKWorkoutActivityTypeCooldown", + "rawValue": 80 + }, + { + "name": "swimBikeRun", + "swiftName": "HKWorkoutActivityTypeSwimBikeRun", + "rawValue": 82 + }, + { + "name": "transition", + "swiftName": "HKWorkoutActivityTypeTransition", + "rawValue": 83 + }, + { + "name": "underwaterDiving", + "swiftName": "HKWorkoutActivityTypeUnderwaterDiving", + "rawValue": 84 + }, + { + "name": "other", + "swiftName": "HKWorkoutActivityTypeOther", + "rawValue": 3000 + } + ] + }, + { + "name": "WorkoutEventType", + "ios": "8.0", + "members": [ + { + "name": "pause", + "swiftName": "HKWorkoutEventTypePause", + "rawValue": 1 + }, + { + "name": "resume", + "swiftName": "HKWorkoutEventTypeResume", + "rawValue": 2 + }, + { + "name": "lap", + "swiftName": "HKWorkoutEventTypeLap", + "rawValue": 3 + }, + { + "name": "marker", + "swiftName": "HKWorkoutEventTypeMarker", + "rawValue": 4 + }, + { + "name": "motionPaused", + "swiftName": "HKWorkoutEventTypeMotionPaused", + "rawValue": 5 + }, + { + "name": "motionResumed", + "swiftName": "HKWorkoutEventTypeMotionResumed", + "rawValue": 6 + }, + { + "name": "segment", + "swiftName": "HKWorkoutEventTypeSegment", + "rawValue": 7 + }, + { + "name": "pauseOrResumeRequest", + "swiftName": "HKWorkoutEventTypePauseOrResumeRequest", + "rawValue": 8 + } + ] + }, + { + "name": "WorkoutSwimmingLocationType", + "ios": "10.0", + "members": [ + { + "name": "unknown", + "swiftName": "HKWorkoutSwimmingLocationTypeUnknown", + "rawValue": 0 + }, + { + "name": "pool", + "swiftName": "HKWorkoutSwimmingLocationTypePool", + "rawValue": 1 + }, + { + "name": "openWater", + "swiftName": "HKWorkoutSwimmingLocationTypeOpenWater", + "rawValue": 2 + } + ] + } + ], + "metadataKeys": [ + { + "keyConstant": "HKMetadataKeyActivityType", + "rawKey": "HKActivityType", + "ios": "17.0", + "expectedType": "an NSNumber containing a HKWorkoutActivityType value", + "tsType": "WorkoutActivityType", + "valueKind": "enum", + "enumName": "WorkoutActivityType", + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAlgorithmVersion", + "rawKey": "HKAlgorithmVersion", + "ios": "15.0", + "expectedType": null, + "tsType": "number", + "valueKind": "number", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAlpineSlopeGrade", + "rawKey": "HKAlpineSlopeGrade", + "ios": "11.2", + "expectedType": "an HKQuantity object compatible with percent unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": ["HKQuantityTypeIdentifierDistanceDownhillSnowSports"] + }, + { + "keyConstant": "HKMetadataKeyAppleDeviceCalibrated", + "rawKey": "HKAppleDeviceCalibrated", + "ios": "14.0", + "expectedType": null, + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAppleECGAlgorithmVersion", + "rawKey": "HKAppleECGAlgorithmVersion", + "ios": "14.0", + "expectedType": "an an NSNumber containing a HKAppleECGAlgorithmVersion value", + "tsType": "AppleECGAlgorithmVersion", + "valueKind": "enum", + "enumName": "AppleECGAlgorithmVersion", + "objectTypes": [], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAppleFitnessPlusCatalogIdentifier", + "rawKey": "HKAppleFitnessPlusCatalogIdentifier", + "ios": "18.2", + "expectedType": "an NSString containing the Fitness+ catalog identifier", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAppleFitnessPlusSession", + "rawKey": "HKAppleFitnessPlusSession", + "ios": "17.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAudioExposureDuration", + "rawKey": "HKAudioExposureDuration", + "ios": "14.2", + "expectedType": "an HKQuantity object compatible with a time unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAudioExposureLevel", + "rawKey": "HKAudioExposureLevel", + "ios": "13.0", + "expectedType": null, + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": [ + "HKCategoryTypeIdentifierAudioExposureEvent", + "HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent", + "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent" + ] + }, + { + "keyConstant": "HKMetadataKeyAverageMETs", + "rawKey": "HKAverageMETs", + "ios": "13.0", + "expectedType": "an HKQuantity expressed in a METs (kcal/(kg*hr)) unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyAverageSpeed", + "rawKey": "HKAverageSpeed", + "ios": "11.2", + "expectedType": "an HKQuantity object compatible with a speed unit (e", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": ["HKQuantityTypeIdentifierDistanceDownhillSnowSports"] + }, + { + "keyConstant": "HKMetadataKeyBarometricPressure", + "rawKey": "HKBarometricPressure", + "ios": "14.0", + "expectedType": "an HKQuantity representing a value in units of pressure (atmospheres, pascals, millimeters of Mercury)", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyBloodGlucoseMealTime", + "rawKey": "HKBloodGlucoseMealTime", + "ios": "11.0", + "expectedType": "an NSNumber containing a HKBloodGlucoseMealTime value", + "tsType": "BloodGlucoseMealTime", + "valueKind": "enum", + "enumName": "BloodGlucoseMealTime", + "objectTypes": ["quantitySample"], + "identifiers": ["HKQuantityTypeIdentifierBloodGlucose"] + }, + { + "keyConstant": "HKMetadataKeyBodyTemperatureSensorLocation", + "rawKey": "HKBodyTemperatureSensorLocation", + "ios": "8.0", + "expectedType": "an NSNumber containing a HKBodyTemperatureSensorLocation value", + "tsType": "BodyTemperatureSensorLocation", + "valueKind": "enum", + "enumName": "BodyTemperatureSensorLocation", + "objectTypes": ["quantitySample"], + "identifiers": [ + "HKQuantityTypeIdentifierBodyTemperature", + "HKQuantityTypeIdentifierBasalBodyTemperature" + ] + }, + { + "keyConstant": "HKMetadataKeyCoachedWorkout", + "rawKey": "HKCoachedWorkout", + "ios": "8.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyCrossTrainerDistance", + "rawKey": "HKCrossTrainerDistance", + "ios": "12.0", + "expectedType": "an HKQuantity object compatible with a length unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyCyclingFunctionalThresholdPowerTestType", + "rawKey": "HKCyclingFunctionalThresholdPowerTestType", + "ios": "17.0", + "expectedType": "an NSNumber containing a HKCyclingFunctionalThresholdPowerTestType value", + "tsType": "CyclingFunctionalThresholdPowerTestType", + "valueKind": "enum", + "enumName": "CyclingFunctionalThresholdPowerTestType", + "objectTypes": [], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDateOfEarliestDataUsedForEstimate", + "rawKey": "HKDateOfEarliestDataUsedForEstimate", + "ios": "15.0", + "expectedType": null, + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDeviceManufacturerName", + "rawKey": "HKDeviceManufacturerName", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDeviceName", + "rawKey": "HKDeviceName", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDevicePlacementSide", + "rawKey": "HKDevicePlacementSide", + "ios": "14.0", + "expectedType": "an NSNumber containing a HKDevicePlacementSide value", + "tsType": "DevicePlacementSide", + "valueKind": "enum", + "enumName": "DevicePlacementSide", + "objectTypes": [], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDeviceSerialNumber", + "rawKey": "HKDeviceSerialNumber", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyDigitalSignature", + "rawKey": "HKDigitalSignature", + "ios": "8.0", + "expectedType": null, + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyElevationAscended", + "rawKey": "HKElevationAscended", + "ios": "11.2", + "expectedType": "an HKQuantity object compatible with length unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyElevationDescended", + "rawKey": "HKElevationDescended", + "ios": "11.2", + "expectedType": "an HKQuantity object compatible with length unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyExternalUUID", + "rawKey": "HKExternalUUID", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyFitnessMachineDuration", + "rawKey": "HKFitnessMachineDuration", + "ios": "12.0", + "expectedType": "an HKQuantity object compatible with a time unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyFoodType", + "rawKey": "HKFoodType", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyGlassesPrescriptionDescription", + "rawKey": "HKGlassesPrescriptionDescription", + "ios": "16.0", + "expectedType": null, + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": [], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyGroupFitness", + "rawKey": "HKGroupFitness", + "ios": "8.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeadphoneGain", + "rawKey": "HKHeadphoneGain", + "ios": "16.4", + "expectedType": null, + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": [ + "HKCategoryTypeIdentifierAudioExposureEvent", + "HKCategoryTypeIdentifierHeadphoneAudioExposureEvent" + ] + }, + { + "keyConstant": "HKMetadataKeyHeartRateEventThreshold", + "rawKey": "HKHeartRateEventThreshold", + "ios": "12.2", + "expectedType": null, + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": [ + "HKCategoryTypeIdentifierHighHeartRateEvent", + "HKCategoryTypeIdentifierLowHeartRateEvent" + ] + }, + { + "keyConstant": "HKMetadataKeyHeartRateMotionContext", + "rawKey": "HKHeartRateMotionContext", + "ios": "11.0", + "expectedType": "an NSNumber containing a HKHeartRateMotionContext value", + "tsType": "HeartRateMotionContext", + "valueKind": "enum", + "enumName": "HeartRateMotionContext", + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeartRateRecoveryActivityDuration", + "rawKey": "HKHeartRateRecoveryActivityDuration", + "ios": "16.0", + "expectedType": "an HKQuantity object compatible with a time unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeartRateRecoveryActivityType", + "rawKey": "HKHeartRateRecoveryActivityType", + "ios": "16.0", + "expectedType": "an NSNumber containing a HKWorkoutActivityType value", + "tsType": "WorkoutActivityType", + "valueKind": "enum", + "enumName": "WorkoutActivityType", + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeartRateRecoveryMaxObservedRecoveryHeartRate", + "rawKey": "HKHeartRateRecoveryMaxObservedRecoveryHeartRate", + "ios": "16.0", + "expectedType": "an HKQuantity object compatible with \"count/min\" unit (eg \"BPM\")", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeartRateRecoveryTestType", + "rawKey": "HKHeartRateRecoveryTestType", + "ios": "16.0", + "expectedType": "an NSNumber containing a HKHeartRateRecoveryTestType value", + "tsType": "HeartRateRecoveryTestType", + "valueKind": "enum", + "enumName": "HeartRateRecoveryTestType", + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyHeartRateSensorLocation", + "rawKey": "HKHeartRateSensorLocation", + "ios": "8.0", + "expectedType": "an NSNumber containing a HKHeartRateSensorLocation value", + "tsType": "HeartRateSensorLocation", + "valueKind": "enum", + "enumName": "HeartRateSensorLocation", + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyIndoorBikeDistance", + "rawKey": "HKIndoorBikeDistance", + "ios": "12.0", + "expectedType": "an HKQuantity object compatible with a length unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyIndoorWorkout", + "rawKey": "HKIndoorWorkout", + "ios": "8.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyInsulinDeliveryReason", + "rawKey": "HKInsulinDeliveryReason", + "ios": "11.0", + "expectedType": "an NSNumber containing a HKInsulinDeliveryReason value", + "tsType": "InsulinDeliveryReason", + "valueKind": "enum", + "enumName": "InsulinDeliveryReason", + "objectTypes": ["quantitySample"], + "identifiers": ["HKQuantityTypeIdentifierInsulinDelivery"] + }, + { + "keyConstant": "HKMetadataKeyLapLength", + "rawKey": "HKLapLength", + "ios": "10.0", + "expectedType": "an HKQuantity object compatible with a length unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyLowCardioFitnessEventThreshold", + "rawKey": "HKLowCardioFitnessEventThreshold", + "ios": "14.3", + "expectedType": null, + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": ["HKCategoryTypeIdentifierLowCardioFitnessEvent"] + }, + { + "keyConstant": "HKMetadataKeyMaximumLightIntensity", + "rawKey": "HKMaximumLightIntensity", + "ios": "17.0", + "expectedType": "an HKQuantity expressed in HKUnit Lux", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyMaximumSpeed", + "rawKey": "HKMaximumSpeed", + "ios": "11.2", + "expectedType": "an HKQuantity object compatible with a speed unit (e", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": ["HKQuantityTypeIdentifierDistanceDownhillSnowSports"] + }, + { + "keyConstant": "HKMetadataKeyMenstrualCycleStart", + "rawKey": "HKMenstrualCycleStart", + "ios": "9.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": ["HKCategoryTypeIdentifierMenstrualFlow"] + }, + { + "keyConstant": "HKMetadataKeyPhysicalEffortEstimationType", + "rawKey": "HKPhysicalEffortEstimationType", + "ios": "17.0", + "expectedType": "an NSNumber containing a HKPhysicalEffortEstimationType value", + "tsType": "PhysicalEffortEstimationType", + "valueKind": "enum", + "enumName": "PhysicalEffortEstimationType", + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyQuantityClampedToLowerBound", + "rawKey": "HKQuantityClampedToLowerBound", + "ios": "16.0", + "expectedType": null, + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyQuantityClampedToUpperBound", + "rawKey": "HKQuantityClampedToUpperBound", + "ios": "16.0", + "expectedType": null, + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyReferenceRangeLowerLimit", + "rawKey": "HKReferenceRangeLowerLimit", + "ios": "8.0", + "expectedType": "an NSNumber", + "tsType": "number", + "valueKind": "number", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyReferenceRangeUpperLimit", + "rawKey": "HKReferenceRangeUpperLimit", + "ios": "8.0", + "expectedType": "an NSNumber", + "tsType": "number", + "valueKind": "number", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySessionEstimate", + "rawKey": "HKSessionEstimate", + "ios": "16.0", + "expectedType": "an HKQuantity object with a unit compatible with the associated HKQuantitySample", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["quantitySample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySexualActivityProtectionUsed", + "rawKey": "HKSexualActivityProtectionUsed", + "ios": "9.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": ["HKCategoryTypeIdentifierSexualActivity"] + }, + { + "keyConstant": "HKMetadataKeySwimmingLocationType", + "rawKey": "HKSwimmingLocationType", + "ios": "10.0", + "expectedType": "an NSNumber containing an HKWorkoutSwimmingLocationType value", + "tsType": "WorkoutSwimmingLocationType", + "valueKind": "enum", + "enumName": "WorkoutSwimmingLocationType", + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySwimmingStrokeStyle", + "rawKey": "HKSwimmingStrokeStyle", + "ios": "10.0", + "expectedType": "an NSNumber containing an HKSwimmingStrokeStyle value", + "tsType": "SwimmingStrokeStyle", + "valueKind": "enum", + "enumName": "SwimmingStrokeStyle", + "objectTypes": ["workoutEvent"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySWOLFScore", + "rawKey": "HKSWOLFScore", + "ios": "16.0", + "expectedType": "an NSNumber containing a score", + "tsType": "number", + "valueKind": "number", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySyncIdentifier", + "rawKey": "HKSyncIdentifier", + "ios": "11.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeySyncVersion", + "rawKey": "HKSyncVersion", + "ios": "11.0", + "expectedType": "NSNumber", + "tsType": "number", + "valueKind": "number", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyTimeZone", + "rawKey": "HKTimeZone", + "ios": "8.0", + "expectedType": "an NSString compatible with NSTimeZone's +timeZoneWithName:", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyUDIDeviceIdentifier", + "rawKey": "HKUDIDeviceIdentifier", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyUDIProductionIdentifier", + "rawKey": "HKUDIProductionIdentifier", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyUserMotionContext", + "rawKey": "HKUserMotionContext", + "ios": "16.0", + "expectedType": "an NSNumber containing a HKUserMotionContext value", + "tsType": "UserMotionContext", + "valueKind": "enum", + "enumName": "UserMotionContext", + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyVO2MaxTestType", + "rawKey": "HKVO2MaxTestType", + "ios": "11.0", + "expectedType": "an NSNumber containing a HKVO2MaxTestType value", + "tsType": "VO2MaxTestType", + "valueKind": "enum", + "enumName": "VO2MaxTestType", + "objectTypes": ["quantitySample"], + "identifiers": ["HKQuantityTypeIdentifierVO2Max"] + }, + { + "keyConstant": "HKMetadataKeyVO2MaxValue", + "rawKey": "HKVO2MaxValue", + "ios": "14.3", + "expectedType": null, + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["categorySample"], + "identifiers": ["HKCategoryTypeIdentifierLowCardioFitnessEvent"] + }, + { + "keyConstant": "HKMetadataKeyWasTakenInLab", + "rawKey": "HKWasTakenInLab", + "ios": "8.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWasUserEntered", + "rawKey": "HKWasUserEntered", + "ios": "8.0", + "expectedType": "an NSNumber containing a BOOL value", + "tsType": "boolean", + "valueKind": "boolean", + "enumName": null, + "objectTypes": ["common"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWaterSalinity", + "rawKey": "HKWaterSalinity", + "ios": "17.0", + "expectedType": "an NSNumber containing a HKWaterSalinity value", + "tsType": "WaterSalinity", + "valueKind": "enum", + "enumName": "WaterSalinity", + "objectTypes": ["sample"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWeatherCondition", + "rawKey": "HKWeatherCondition", + "ios": "10.0", + "expectedType": "an NSNumber containing an HKWeatherCondition value", + "tsType": "WeatherCondition", + "valueKind": "enum", + "enumName": "WeatherCondition", + "objectTypes": ["sample", "workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWeatherHumidity", + "rawKey": "HKWeatherHumidity", + "ios": "10.0", + "expectedType": "an HKQuantity expressed in percent", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["sample", "workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWeatherTemperature", + "rawKey": "HKWeatherTemperature", + "ios": "10.0", + "expectedType": "an HKQuantity expressed in a temperature unit", + "tsType": "Quantity", + "valueKind": "quantity", + "enumName": null, + "objectTypes": ["sample", "workout"], + "identifiers": [] + }, + { + "keyConstant": "HKMetadataKeyWorkoutBrandName", + "rawKey": "HKWorkoutBrandName", + "ios": "8.0", + "expectedType": "NSString", + "tsType": "string", + "valueKind": "string", + "enumName": null, + "objectTypes": ["workout"], + "identifiers": [] + } + ] +} diff --git a/packages/react-native-healthkit/src/generated/healthkit.generated.ts b/packages/react-native-healthkit/src/generated/healthkit.generated.ts new file mode 100644 index 00000000..a2935ff8 --- /dev/null +++ b/packages/react-native-healthkit/src/generated/healthkit.generated.ts @@ -0,0 +1,1307 @@ +/* + * AUTO-GENERATED FILE. DO NOT EDIT. + * Source: scripts/generate-healthkit.ts + */ + +import type { Quantity } from '../types/QuantityType' +import type { + BloodGlucoseUnit, + CountPerTime, + EnergyUnit, + LengthUnit, + MassUnit, + PressureUnit, + SpeedUnit, + TemperatureUnit, + TimeUnit, +} from '../types/Units' +export type QuantityTypeIdentifierReadOnly = + | 'HKQuantityTypeIdentifierAppleExerciseTime' + | 'HKQuantityTypeIdentifierAppleStandTime' + | 'HKQuantityTypeIdentifierAppleWalkingSteadiness' + | 'HKQuantityTypeIdentifierAtrialFibrillationBurden' + | 'HKQuantityTypeIdentifierWalkingHeartRateAverage' +export type QuantityTypeIdentifierWriteable = + | 'HKQuantityTypeIdentifierActiveEnergyBurned' + | 'HKQuantityTypeIdentifierAppleMoveTime' + | 'HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances' + | 'HKQuantityTypeIdentifierAppleSleepingWristTemperature' + | 'HKQuantityTypeIdentifierBasalBodyTemperature' + | 'HKQuantityTypeIdentifierBasalEnergyBurned' + | 'HKQuantityTypeIdentifierBloodAlcoholContent' + | 'HKQuantityTypeIdentifierBloodGlucose' + | 'HKQuantityTypeIdentifierBloodPressureDiastolic' + | 'HKQuantityTypeIdentifierBloodPressureSystolic' + | 'HKQuantityTypeIdentifierBodyFatPercentage' + | 'HKQuantityTypeIdentifierBodyMass' + | 'HKQuantityTypeIdentifierBodyMassIndex' + | 'HKQuantityTypeIdentifierBodyTemperature' + | 'HKQuantityTypeIdentifierCrossCountrySkiingSpeed' + | 'HKQuantityTypeIdentifierCyclingCadence' + | 'HKQuantityTypeIdentifierCyclingFunctionalThresholdPower' + | 'HKQuantityTypeIdentifierCyclingPower' + | 'HKQuantityTypeIdentifierCyclingSpeed' + | 'HKQuantityTypeIdentifierDietaryBiotin' + | 'HKQuantityTypeIdentifierDietaryCaffeine' + | 'HKQuantityTypeIdentifierDietaryCalcium' + | 'HKQuantityTypeIdentifierDietaryCarbohydrates' + | 'HKQuantityTypeIdentifierDietaryChloride' + | 'HKQuantityTypeIdentifierDietaryCholesterol' + | 'HKQuantityTypeIdentifierDietaryChromium' + | 'HKQuantityTypeIdentifierDietaryCopper' + | 'HKQuantityTypeIdentifierDietaryEnergyConsumed' + | 'HKQuantityTypeIdentifierDietaryFatMonounsaturated' + | 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated' + | 'HKQuantityTypeIdentifierDietaryFatSaturated' + | 'HKQuantityTypeIdentifierDietaryFatTotal' + | 'HKQuantityTypeIdentifierDietaryFiber' + | 'HKQuantityTypeIdentifierDietaryFolate' + | 'HKQuantityTypeIdentifierDietaryIodine' + | 'HKQuantityTypeIdentifierDietaryIron' + | 'HKQuantityTypeIdentifierDietaryMagnesium' + | 'HKQuantityTypeIdentifierDietaryManganese' + | 'HKQuantityTypeIdentifierDietaryMolybdenum' + | 'HKQuantityTypeIdentifierDietaryNiacin' + | 'HKQuantityTypeIdentifierDietaryPantothenicAcid' + | 'HKQuantityTypeIdentifierDietaryPhosphorus' + | 'HKQuantityTypeIdentifierDietaryPotassium' + | 'HKQuantityTypeIdentifierDietaryProtein' + | 'HKQuantityTypeIdentifierDietaryRiboflavin' + | 'HKQuantityTypeIdentifierDietarySelenium' + | 'HKQuantityTypeIdentifierDietarySodium' + | 'HKQuantityTypeIdentifierDietarySugar' + | 'HKQuantityTypeIdentifierDietaryThiamin' + | 'HKQuantityTypeIdentifierDietaryVitaminA' + | 'HKQuantityTypeIdentifierDietaryVitaminB12' + | 'HKQuantityTypeIdentifierDietaryVitaminB6' + | 'HKQuantityTypeIdentifierDietaryVitaminC' + | 'HKQuantityTypeIdentifierDietaryVitaminD' + | 'HKQuantityTypeIdentifierDietaryVitaminE' + | 'HKQuantityTypeIdentifierDietaryVitaminK' + | 'HKQuantityTypeIdentifierDietaryWater' + | 'HKQuantityTypeIdentifierDietaryZinc' + | 'HKQuantityTypeIdentifierDistanceCrossCountrySkiing' + | 'HKQuantityTypeIdentifierDistanceCycling' + | 'HKQuantityTypeIdentifierDistanceDownhillSnowSports' + | 'HKQuantityTypeIdentifierDistancePaddleSports' + | 'HKQuantityTypeIdentifierDistanceRowing' + | 'HKQuantityTypeIdentifierDistanceSkatingSports' + | 'HKQuantityTypeIdentifierDistanceSwimming' + | 'HKQuantityTypeIdentifierDistanceWalkingRunning' + | 'HKQuantityTypeIdentifierDistanceWheelchair' + | 'HKQuantityTypeIdentifierElectrodermalActivity' + | 'HKQuantityTypeIdentifierEnvironmentalAudioExposure' + | 'HKQuantityTypeIdentifierEnvironmentalSoundReduction' + | 'HKQuantityTypeIdentifierEstimatedWorkoutEffortScore' + | 'HKQuantityTypeIdentifierFlightsClimbed' + | 'HKQuantityTypeIdentifierForcedExpiratoryVolume1' + | 'HKQuantityTypeIdentifierForcedVitalCapacity' + | 'HKQuantityTypeIdentifierHeadphoneAudioExposure' + | 'HKQuantityTypeIdentifierHeartRate' + | 'HKQuantityTypeIdentifierHeartRateRecoveryOneMinute' + | 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN' + | 'HKQuantityTypeIdentifierHeight' + | 'HKQuantityTypeIdentifierInhalerUsage' + | 'HKQuantityTypeIdentifierInsulinDelivery' + | 'HKQuantityTypeIdentifierLeanBodyMass' + | 'HKQuantityTypeIdentifierNikeFuel' + | 'HKQuantityTypeIdentifierNumberOfAlcoholicBeverages' + | 'HKQuantityTypeIdentifierNumberOfTimesFallen' + | 'HKQuantityTypeIdentifierOxygenSaturation' + | 'HKQuantityTypeIdentifierPaddleSportsSpeed' + | 'HKQuantityTypeIdentifierPeakExpiratoryFlowRate' + | 'HKQuantityTypeIdentifierPeripheralPerfusionIndex' + | 'HKQuantityTypeIdentifierPhysicalEffort' + | 'HKQuantityTypeIdentifierPushCount' + | 'HKQuantityTypeIdentifierRespiratoryRate' + | 'HKQuantityTypeIdentifierRestingHeartRate' + | 'HKQuantityTypeIdentifierRowingSpeed' + | 'HKQuantityTypeIdentifierRunningGroundContactTime' + | 'HKQuantityTypeIdentifierRunningPower' + | 'HKQuantityTypeIdentifierRunningSpeed' + | 'HKQuantityTypeIdentifierRunningStrideLength' + | 'HKQuantityTypeIdentifierRunningVerticalOscillation' + | 'HKQuantityTypeIdentifierSixMinuteWalkTestDistance' + | 'HKQuantityTypeIdentifierStairAscentSpeed' + | 'HKQuantityTypeIdentifierStairDescentSpeed' + | 'HKQuantityTypeIdentifierStepCount' + | 'HKQuantityTypeIdentifierSwimmingStrokeCount' + | 'HKQuantityTypeIdentifierTimeInDaylight' + | 'HKQuantityTypeIdentifierUnderwaterDepth' + | 'HKQuantityTypeIdentifierUVExposure' + | 'HKQuantityTypeIdentifierVO2Max' + | 'HKQuantityTypeIdentifierWaistCircumference' + | 'HKQuantityTypeIdentifierWalkingAsymmetryPercentage' + | 'HKQuantityTypeIdentifierWalkingDoubleSupportPercentage' + | 'HKQuantityTypeIdentifierWalkingSpeed' + | 'HKQuantityTypeIdentifierWalkingStepLength' + | 'HKQuantityTypeIdentifierWaterTemperature' + | 'HKQuantityTypeIdentifierWorkoutEffortScore' +export type QuantityTypeIdentifier = + | QuantityTypeIdentifierReadOnly + | QuantityTypeIdentifierWriteable +export type CategoryTypeIdentifierReadOnly = + | 'HKCategoryTypeIdentifierAppleStandHour' + | 'HKCategoryTypeIdentifierHeadphoneAudioExposureEvent' + | 'HKCategoryTypeIdentifierHighHeartRateEvent' + | 'HKCategoryTypeIdentifierHypertensionEvent' + | 'HKCategoryTypeIdentifierLowHeartRateEvent' +export type CategoryTypeIdentifierWriteable = + | 'HKCategoryTypeIdentifierAbdominalCramps' + | 'HKCategoryTypeIdentifierAcne' + | 'HKCategoryTypeIdentifierAppetiteChanges' + | 'HKCategoryTypeIdentifierAppleWalkingSteadinessEvent' + | 'HKCategoryTypeIdentifierAudioExposureEvent' + | 'HKCategoryTypeIdentifierBladderIncontinence' + | 'HKCategoryTypeIdentifierBleedingAfterPregnancy' + | 'HKCategoryTypeIdentifierBleedingDuringPregnancy' + | 'HKCategoryTypeIdentifierBloating' + | 'HKCategoryTypeIdentifierBreastPain' + | 'HKCategoryTypeIdentifierCervicalMucusQuality' + | 'HKCategoryTypeIdentifierChestTightnessOrPain' + | 'HKCategoryTypeIdentifierChills' + | 'HKCategoryTypeIdentifierConstipation' + | 'HKCategoryTypeIdentifierContraceptive' + | 'HKCategoryTypeIdentifierCoughing' + | 'HKCategoryTypeIdentifierDiarrhea' + | 'HKCategoryTypeIdentifierDizziness' + | 'HKCategoryTypeIdentifierDrySkin' + | 'HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent' + | 'HKCategoryTypeIdentifierFainting' + | 'HKCategoryTypeIdentifierFatigue' + | 'HKCategoryTypeIdentifierFever' + | 'HKCategoryTypeIdentifierGeneralizedBodyAche' + | 'HKCategoryTypeIdentifierHairLoss' + | 'HKCategoryTypeIdentifierHandwashingEvent' + | 'HKCategoryTypeIdentifierHeadache' + | 'HKCategoryTypeIdentifierHeartburn' + | 'HKCategoryTypeIdentifierHotFlashes' + | 'HKCategoryTypeIdentifierInfrequentMenstrualCycles' + | 'HKCategoryTypeIdentifierIntermenstrualBleeding' + | 'HKCategoryTypeIdentifierIrregularHeartRhythmEvent' + | 'HKCategoryTypeIdentifierIrregularMenstrualCycles' + | 'HKCategoryTypeIdentifierLactation' + | 'HKCategoryTypeIdentifierLossOfSmell' + | 'HKCategoryTypeIdentifierLossOfTaste' + | 'HKCategoryTypeIdentifierLowCardioFitnessEvent' + | 'HKCategoryTypeIdentifierLowerBackPain' + | 'HKCategoryTypeIdentifierMemoryLapse' + | 'HKCategoryTypeIdentifierMenstrualFlow' + | 'HKCategoryTypeIdentifierMindfulSession' + | 'HKCategoryTypeIdentifierMoodChanges' + | 'HKCategoryTypeIdentifierNausea' + | 'HKCategoryTypeIdentifierNightSweats' + | 'HKCategoryTypeIdentifierOvulationTestResult' + | 'HKCategoryTypeIdentifierPelvicPain' + | 'HKCategoryTypeIdentifierPersistentIntermenstrualBleeding' + | 'HKCategoryTypeIdentifierPregnancy' + | 'HKCategoryTypeIdentifierPregnancyTestResult' + | 'HKCategoryTypeIdentifierProgesteroneTestResult' + | 'HKCategoryTypeIdentifierProlongedMenstrualPeriods' + | 'HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat' + | 'HKCategoryTypeIdentifierRunnyNose' + | 'HKCategoryTypeIdentifierSexualActivity' + | 'HKCategoryTypeIdentifierShortnessOfBreath' + | 'HKCategoryTypeIdentifierSinusCongestion' + | 'HKCategoryTypeIdentifierSkippedHeartbeat' + | 'HKCategoryTypeIdentifierSleepAnalysis' + | 'HKCategoryTypeIdentifierSleepApneaEvent' + | 'HKCategoryTypeIdentifierSleepChanges' + | 'HKCategoryTypeIdentifierSoreThroat' + | 'HKCategoryTypeIdentifierToothbrushingEvent' + | 'HKCategoryTypeIdentifierVaginalDryness' + | 'HKCategoryTypeIdentifierVomiting' + | 'HKCategoryTypeIdentifierWheezing' +export type CategoryTypeIdentifier = + | CategoryTypeIdentifierReadOnly + | CategoryTypeIdentifierWriteable +export const QUANTITY_IDENTIFIER_IOS_AVAILABILITY = { + HKQuantityTypeIdentifierActiveEnergyBurned: '8.0', + HKQuantityTypeIdentifierAppleExerciseTime: '9.3', + HKQuantityTypeIdentifierAppleMoveTime: '14.5', + HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances: '18.0', + HKQuantityTypeIdentifierAppleSleepingWristTemperature: '16.0', + HKQuantityTypeIdentifierAppleStandTime: '13.0', + HKQuantityTypeIdentifierAppleWalkingSteadiness: '15.0', + HKQuantityTypeIdentifierAtrialFibrillationBurden: '16.0', + HKQuantityTypeIdentifierBasalBodyTemperature: '9.0', + HKQuantityTypeIdentifierBasalEnergyBurned: '8.0', + HKQuantityTypeIdentifierBloodAlcoholContent: '8.0', + HKQuantityTypeIdentifierBloodGlucose: '8.0', + HKQuantityTypeIdentifierBloodPressureDiastolic: '8.0', + HKQuantityTypeIdentifierBloodPressureSystolic: '8.0', + HKQuantityTypeIdentifierBodyFatPercentage: '8.0', + HKQuantityTypeIdentifierBodyMass: '8.0', + HKQuantityTypeIdentifierBodyMassIndex: '8.0', + HKQuantityTypeIdentifierBodyTemperature: '8.0', + HKQuantityTypeIdentifierCrossCountrySkiingSpeed: '18.0', + HKQuantityTypeIdentifierCyclingCadence: '17.0', + HKQuantityTypeIdentifierCyclingFunctionalThresholdPower: '17.0', + HKQuantityTypeIdentifierCyclingPower: '17.0', + HKQuantityTypeIdentifierCyclingSpeed: '17.0', + HKQuantityTypeIdentifierDietaryBiotin: '8.0', + HKQuantityTypeIdentifierDietaryCaffeine: '8.0', + HKQuantityTypeIdentifierDietaryCalcium: '8.0', + HKQuantityTypeIdentifierDietaryCarbohydrates: '8.0', + HKQuantityTypeIdentifierDietaryChloride: '8.0', + HKQuantityTypeIdentifierDietaryCholesterol: '8.0', + HKQuantityTypeIdentifierDietaryChromium: '8.0', + HKQuantityTypeIdentifierDietaryCopper: '8.0', + HKQuantityTypeIdentifierDietaryEnergyConsumed: '8.0', + HKQuantityTypeIdentifierDietaryFatMonounsaturated: '8.0', + HKQuantityTypeIdentifierDietaryFatPolyunsaturated: '8.0', + HKQuantityTypeIdentifierDietaryFatSaturated: '8.0', + HKQuantityTypeIdentifierDietaryFatTotal: '8.0', + HKQuantityTypeIdentifierDietaryFiber: '8.0', + HKQuantityTypeIdentifierDietaryFolate: '8.0', + HKQuantityTypeIdentifierDietaryIodine: '8.0', + HKQuantityTypeIdentifierDietaryIron: '8.0', + HKQuantityTypeIdentifierDietaryMagnesium: '8.0', + HKQuantityTypeIdentifierDietaryManganese: '8.0', + HKQuantityTypeIdentifierDietaryMolybdenum: '8.0', + HKQuantityTypeIdentifierDietaryNiacin: '8.0', + HKQuantityTypeIdentifierDietaryPantothenicAcid: '8.0', + HKQuantityTypeIdentifierDietaryPhosphorus: '8.0', + HKQuantityTypeIdentifierDietaryPotassium: '8.0', + HKQuantityTypeIdentifierDietaryProtein: '8.0', + HKQuantityTypeIdentifierDietaryRiboflavin: '8.0', + HKQuantityTypeIdentifierDietarySelenium: '8.0', + HKQuantityTypeIdentifierDietarySodium: '8.0', + HKQuantityTypeIdentifierDietarySugar: '8.0', + HKQuantityTypeIdentifierDietaryThiamin: '8.0', + HKQuantityTypeIdentifierDietaryVitaminA: '8.0', + HKQuantityTypeIdentifierDietaryVitaminB12: '8.0', + HKQuantityTypeIdentifierDietaryVitaminB6: '8.0', + HKQuantityTypeIdentifierDietaryVitaminC: '8.0', + HKQuantityTypeIdentifierDietaryVitaminD: '8.0', + HKQuantityTypeIdentifierDietaryVitaminE: '8.0', + HKQuantityTypeIdentifierDietaryVitaminK: '8.0', + HKQuantityTypeIdentifierDietaryWater: '9.0', + HKQuantityTypeIdentifierDietaryZinc: '8.0', + HKQuantityTypeIdentifierDistanceCrossCountrySkiing: '18.0', + HKQuantityTypeIdentifierDistanceCycling: '8.0', + HKQuantityTypeIdentifierDistanceDownhillSnowSports: '11.2', + HKQuantityTypeIdentifierDistancePaddleSports: '18.0', + HKQuantityTypeIdentifierDistanceRowing: '18.0', + HKQuantityTypeIdentifierDistanceSkatingSports: '18.0', + HKQuantityTypeIdentifierDistanceSwimming: '10.0', + HKQuantityTypeIdentifierDistanceWalkingRunning: '8.0', + HKQuantityTypeIdentifierDistanceWheelchair: '10.0', + HKQuantityTypeIdentifierElectrodermalActivity: '8.0', + HKQuantityTypeIdentifierEnvironmentalAudioExposure: '13.0', + HKQuantityTypeIdentifierEnvironmentalSoundReduction: '16.0', + HKQuantityTypeIdentifierEstimatedWorkoutEffortScore: '18.0', + HKQuantityTypeIdentifierFlightsClimbed: '8.0', + HKQuantityTypeIdentifierForcedExpiratoryVolume1: '8.0', + HKQuantityTypeIdentifierForcedVitalCapacity: '8.0', + HKQuantityTypeIdentifierHeadphoneAudioExposure: '13.0', + HKQuantityTypeIdentifierHeartRate: '8.0', + HKQuantityTypeIdentifierHeartRateRecoveryOneMinute: '16.0', + HKQuantityTypeIdentifierHeartRateVariabilitySDNN: '11.0', + HKQuantityTypeIdentifierHeight: '8.0', + HKQuantityTypeIdentifierInhalerUsage: '8.0', + HKQuantityTypeIdentifierInsulinDelivery: '11.0', + HKQuantityTypeIdentifierLeanBodyMass: '8.0', + HKQuantityTypeIdentifierNikeFuel: '8.0', + HKQuantityTypeIdentifierNumberOfAlcoholicBeverages: '15.0', + HKQuantityTypeIdentifierNumberOfTimesFallen: '8.0', + HKQuantityTypeIdentifierOxygenSaturation: '8.0', + HKQuantityTypeIdentifierPaddleSportsSpeed: '18.0', + HKQuantityTypeIdentifierPeakExpiratoryFlowRate: '8.0', + HKQuantityTypeIdentifierPeripheralPerfusionIndex: '8.0', + HKQuantityTypeIdentifierPhysicalEffort: '17.0', + HKQuantityTypeIdentifierPushCount: '10.0', + HKQuantityTypeIdentifierRespiratoryRate: '8.0', + HKQuantityTypeIdentifierRestingHeartRate: '11.0', + HKQuantityTypeIdentifierRowingSpeed: '18.0', + HKQuantityTypeIdentifierRunningGroundContactTime: '16.0', + HKQuantityTypeIdentifierRunningPower: '16.0', + HKQuantityTypeIdentifierRunningSpeed: '16.0', + HKQuantityTypeIdentifierRunningStrideLength: '16.0', + HKQuantityTypeIdentifierRunningVerticalOscillation: '16.0', + HKQuantityTypeIdentifierSixMinuteWalkTestDistance: '14.0', + HKQuantityTypeIdentifierStairAscentSpeed: '14.0', + HKQuantityTypeIdentifierStairDescentSpeed: '14.0', + HKQuantityTypeIdentifierStepCount: '8.0', + HKQuantityTypeIdentifierSwimmingStrokeCount: '10.0', + HKQuantityTypeIdentifierTimeInDaylight: '17.0', + HKQuantityTypeIdentifierUnderwaterDepth: '16.0', + HKQuantityTypeIdentifierUVExposure: '9.0', + HKQuantityTypeIdentifierVO2Max: '11.0', + HKQuantityTypeIdentifierWaistCircumference: '11.0', + HKQuantityTypeIdentifierWalkingAsymmetryPercentage: '14.0', + HKQuantityTypeIdentifierWalkingDoubleSupportPercentage: '14.0', + HKQuantityTypeIdentifierWalkingHeartRateAverage: '11.0', + HKQuantityTypeIdentifierWalkingSpeed: '14.0', + HKQuantityTypeIdentifierWalkingStepLength: '14.0', + HKQuantityTypeIdentifierWaterTemperature: '16.0', + HKQuantityTypeIdentifierWorkoutEffortScore: '18.0', +} as const +export const CATEGORY_IDENTIFIER_IOS_AVAILABILITY = { + HKCategoryTypeIdentifierAbdominalCramps: '13.6', + HKCategoryTypeIdentifierAcne: '13.6', + HKCategoryTypeIdentifierAppetiteChanges: '13.6', + HKCategoryTypeIdentifierAppleStandHour: '9.0', + HKCategoryTypeIdentifierAppleWalkingSteadinessEvent: '15.0', + HKCategoryTypeIdentifierAudioExposureEvent: '13.0', + HKCategoryTypeIdentifierBladderIncontinence: '14.0', + HKCategoryTypeIdentifierBleedingAfterPregnancy: '18.0', + HKCategoryTypeIdentifierBleedingDuringPregnancy: '18.0', + HKCategoryTypeIdentifierBloating: '13.6', + HKCategoryTypeIdentifierBreastPain: '13.6', + HKCategoryTypeIdentifierCervicalMucusQuality: '9.0', + HKCategoryTypeIdentifierChestTightnessOrPain: '13.6', + HKCategoryTypeIdentifierChills: '13.6', + HKCategoryTypeIdentifierConstipation: '13.6', + HKCategoryTypeIdentifierContraceptive: '14.3', + HKCategoryTypeIdentifierCoughing: '13.6', + HKCategoryTypeIdentifierDiarrhea: '13.6', + HKCategoryTypeIdentifierDizziness: '13.6', + HKCategoryTypeIdentifierDrySkin: '14.0', + HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent: '14.0', + HKCategoryTypeIdentifierFainting: '13.6', + HKCategoryTypeIdentifierFatigue: '13.6', + HKCategoryTypeIdentifierFever: '13.6', + HKCategoryTypeIdentifierGeneralizedBodyAche: '13.6', + HKCategoryTypeIdentifierHairLoss: '14.0', + HKCategoryTypeIdentifierHandwashingEvent: '14.0', + HKCategoryTypeIdentifierHeadache: '13.6', + HKCategoryTypeIdentifierHeadphoneAudioExposureEvent: '14.2', + HKCategoryTypeIdentifierHeartburn: '13.6', + HKCategoryTypeIdentifierHighHeartRateEvent: '12.2', + HKCategoryTypeIdentifierHotFlashes: '13.6', + HKCategoryTypeIdentifierHypertensionEvent: '26.2', + HKCategoryTypeIdentifierInfrequentMenstrualCycles: '16.0', + HKCategoryTypeIdentifierIntermenstrualBleeding: '9.0', + HKCategoryTypeIdentifierIrregularHeartRhythmEvent: '12.2', + HKCategoryTypeIdentifierIrregularMenstrualCycles: '16.0', + HKCategoryTypeIdentifierLactation: '14.3', + HKCategoryTypeIdentifierLossOfSmell: '13.6', + HKCategoryTypeIdentifierLossOfTaste: '13.6', + HKCategoryTypeIdentifierLowCardioFitnessEvent: '14.3', + HKCategoryTypeIdentifierLowerBackPain: '13.6', + HKCategoryTypeIdentifierLowHeartRateEvent: '12.2', + HKCategoryTypeIdentifierMemoryLapse: '14.0', + HKCategoryTypeIdentifierMenstrualFlow: '9.0', + HKCategoryTypeIdentifierMindfulSession: '10.0', + HKCategoryTypeIdentifierMoodChanges: '13.6', + HKCategoryTypeIdentifierNausea: '13.6', + HKCategoryTypeIdentifierNightSweats: '14.0', + HKCategoryTypeIdentifierOvulationTestResult: '9.0', + HKCategoryTypeIdentifierPelvicPain: '13.6', + HKCategoryTypeIdentifierPersistentIntermenstrualBleeding: '16.0', + HKCategoryTypeIdentifierPregnancy: '14.3', + HKCategoryTypeIdentifierPregnancyTestResult: '15.0', + HKCategoryTypeIdentifierProgesteroneTestResult: '15.0', + HKCategoryTypeIdentifierProlongedMenstrualPeriods: '16.0', + HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat: '13.6', + HKCategoryTypeIdentifierRunnyNose: '13.6', + HKCategoryTypeIdentifierSexualActivity: '9.0', + HKCategoryTypeIdentifierShortnessOfBreath: '13.6', + HKCategoryTypeIdentifierSinusCongestion: '13.6', + HKCategoryTypeIdentifierSkippedHeartbeat: '13.6', + HKCategoryTypeIdentifierSleepAnalysis: '8.0', + HKCategoryTypeIdentifierSleepApneaEvent: '18.0', + HKCategoryTypeIdentifierSleepChanges: '13.6', + HKCategoryTypeIdentifierSoreThroat: '13.6', + HKCategoryTypeIdentifierToothbrushingEvent: '13.0', + HKCategoryTypeIdentifierVaginalDryness: '14.0', + HKCategoryTypeIdentifierVomiting: '13.6', + HKCategoryTypeIdentifierWheezing: '13.6', +} as const +export const QUANTITY_IDENTIFIER_CANONICAL_UNITS = { + HKQuantityTypeIdentifierActiveEnergyBurned: 'kcal', + HKQuantityTypeIdentifierAppleExerciseTime: 'min', + HKQuantityTypeIdentifierAppleMoveTime: 'min', + HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances: 'count', + HKQuantityTypeIdentifierAppleSleepingWristTemperature: 'degC', + HKQuantityTypeIdentifierAppleStandTime: 'min', + HKQuantityTypeIdentifierAppleWalkingSteadiness: '%', + HKQuantityTypeIdentifierAtrialFibrillationBurden: '%', + HKQuantityTypeIdentifierBasalBodyTemperature: 'degC', + HKQuantityTypeIdentifierBasalEnergyBurned: 'kcal', + HKQuantityTypeIdentifierBloodAlcoholContent: '%', + HKQuantityTypeIdentifierBloodGlucose: 'mg/dL', + HKQuantityTypeIdentifierBloodPressureDiastolic: 'mmHg', + HKQuantityTypeIdentifierBloodPressureSystolic: 'mmHg', + HKQuantityTypeIdentifierBodyFatPercentage: '%', + HKQuantityTypeIdentifierBodyMass: 'kg', + HKQuantityTypeIdentifierBodyMassIndex: 'count', + HKQuantityTypeIdentifierBodyTemperature: 'degC', + HKQuantityTypeIdentifierCrossCountrySkiingSpeed: 'm/s', + HKQuantityTypeIdentifierCyclingCadence: 'count/min', + HKQuantityTypeIdentifierCyclingFunctionalThresholdPower: 'W', + HKQuantityTypeIdentifierCyclingPower: 'W', + HKQuantityTypeIdentifierCyclingSpeed: 'm/s', + HKQuantityTypeIdentifierDietaryBiotin: 'g', + HKQuantityTypeIdentifierDietaryCaffeine: 'g', + HKQuantityTypeIdentifierDietaryCalcium: 'g', + HKQuantityTypeIdentifierDietaryCarbohydrates: 'g', + HKQuantityTypeIdentifierDietaryChloride: 'g', + HKQuantityTypeIdentifierDietaryCholesterol: 'g', + HKQuantityTypeIdentifierDietaryChromium: 'g', + HKQuantityTypeIdentifierDietaryCopper: 'g', + HKQuantityTypeIdentifierDietaryEnergyConsumed: 'kcal', + HKQuantityTypeIdentifierDietaryFatMonounsaturated: 'g', + HKQuantityTypeIdentifierDietaryFatPolyunsaturated: 'g', + HKQuantityTypeIdentifierDietaryFatSaturated: 'g', + HKQuantityTypeIdentifierDietaryFatTotal: 'g', + HKQuantityTypeIdentifierDietaryFiber: 'g', + HKQuantityTypeIdentifierDietaryFolate: 'g', + HKQuantityTypeIdentifierDietaryIodine: 'g', + HKQuantityTypeIdentifierDietaryIron: 'g', + HKQuantityTypeIdentifierDietaryMagnesium: 'g', + HKQuantityTypeIdentifierDietaryManganese: 'g', + HKQuantityTypeIdentifierDietaryMolybdenum: 'g', + HKQuantityTypeIdentifierDietaryNiacin: 'g', + HKQuantityTypeIdentifierDietaryPantothenicAcid: 'g', + HKQuantityTypeIdentifierDietaryPhosphorus: 'g', + HKQuantityTypeIdentifierDietaryPotassium: 'g', + HKQuantityTypeIdentifierDietaryProtein: 'g', + HKQuantityTypeIdentifierDietaryRiboflavin: 'g', + HKQuantityTypeIdentifierDietarySelenium: 'g', + HKQuantityTypeIdentifierDietarySodium: 'g', + HKQuantityTypeIdentifierDietarySugar: 'g', + HKQuantityTypeIdentifierDietaryThiamin: 'g', + HKQuantityTypeIdentifierDietaryVitaminA: 'g', + HKQuantityTypeIdentifierDietaryVitaminB12: 'g', + HKQuantityTypeIdentifierDietaryVitaminB6: 'g', + HKQuantityTypeIdentifierDietaryVitaminC: 'g', + HKQuantityTypeIdentifierDietaryVitaminD: 'g', + HKQuantityTypeIdentifierDietaryVitaminE: 'g', + HKQuantityTypeIdentifierDietaryVitaminK: 'g', + HKQuantityTypeIdentifierDietaryWater: 'mL', + HKQuantityTypeIdentifierDietaryZinc: 'g', + HKQuantityTypeIdentifierDistanceCrossCountrySkiing: 'm', + HKQuantityTypeIdentifierDistanceCycling: 'm', + HKQuantityTypeIdentifierDistanceDownhillSnowSports: 'm', + HKQuantityTypeIdentifierDistancePaddleSports: 'm', + HKQuantityTypeIdentifierDistanceRowing: 'm', + HKQuantityTypeIdentifierDistanceSkatingSports: 'm', + HKQuantityTypeIdentifierDistanceSwimming: 'm', + HKQuantityTypeIdentifierDistanceWalkingRunning: 'm', + HKQuantityTypeIdentifierDistanceWheelchair: 'm', + HKQuantityTypeIdentifierElectrodermalActivity: 'S', + HKQuantityTypeIdentifierEnvironmentalAudioExposure: 'dBASPL', + HKQuantityTypeIdentifierEnvironmentalSoundReduction: 'dBASPL', + HKQuantityTypeIdentifierEstimatedWorkoutEffortScore: 'appleEffortScore', + HKQuantityTypeIdentifierFlightsClimbed: 'count', + HKQuantityTypeIdentifierForcedExpiratoryVolume1: 'L', + HKQuantityTypeIdentifierForcedVitalCapacity: 'L', + HKQuantityTypeIdentifierHeadphoneAudioExposure: 'dBASPL', + HKQuantityTypeIdentifierHeartRate: 'count/s', + HKQuantityTypeIdentifierHeartRateRecoveryOneMinute: 'count/min', + HKQuantityTypeIdentifierHeartRateVariabilitySDNN: 'ms', + HKQuantityTypeIdentifierHeight: 'm', + HKQuantityTypeIdentifierInhalerUsage: 'count', + HKQuantityTypeIdentifierInsulinDelivery: 'IU', + HKQuantityTypeIdentifierLeanBodyMass: 'kg', + HKQuantityTypeIdentifierNikeFuel: 'count', + HKQuantityTypeIdentifierNumberOfAlcoholicBeverages: 'count', + HKQuantityTypeIdentifierNumberOfTimesFallen: 'count', + HKQuantityTypeIdentifierOxygenSaturation: '%', + HKQuantityTypeIdentifierPaddleSportsSpeed: 'm/s', + HKQuantityTypeIdentifierPeakExpiratoryFlowRate: 'L/min', + HKQuantityTypeIdentifierPeripheralPerfusionIndex: '%', + HKQuantityTypeIdentifierPhysicalEffort: 'kcal/(kg*hr)', + HKQuantityTypeIdentifierPushCount: 'count', + HKQuantityTypeIdentifierRespiratoryRate: 'count/s', + HKQuantityTypeIdentifierRestingHeartRate: 'count/min', + HKQuantityTypeIdentifierRowingSpeed: 'm/s', + HKQuantityTypeIdentifierRunningGroundContactTime: 'ms', + HKQuantityTypeIdentifierRunningPower: 'W', + HKQuantityTypeIdentifierRunningSpeed: 'm/s', + HKQuantityTypeIdentifierRunningStrideLength: 'm', + HKQuantityTypeIdentifierRunningVerticalOscillation: 'cm', + HKQuantityTypeIdentifierSixMinuteWalkTestDistance: 'm', + HKQuantityTypeIdentifierStairAscentSpeed: 'm/s', + HKQuantityTypeIdentifierStairDescentSpeed: 'm/s', + HKQuantityTypeIdentifierStepCount: 'count', + HKQuantityTypeIdentifierSwimmingStrokeCount: 'count', + HKQuantityTypeIdentifierTimeInDaylight: 'min', + HKQuantityTypeIdentifierUnderwaterDepth: 'm', + HKQuantityTypeIdentifierUVExposure: null, + HKQuantityTypeIdentifierVO2Max: 'ml/(kg*min)', + HKQuantityTypeIdentifierWaistCircumference: 'm', + HKQuantityTypeIdentifierWalkingAsymmetryPercentage: '%', + HKQuantityTypeIdentifierWalkingDoubleSupportPercentage: '%', + HKQuantityTypeIdentifierWalkingHeartRateAverage: 'count/min', + HKQuantityTypeIdentifierWalkingSpeed: 'm/s', + HKQuantityTypeIdentifierWalkingStepLength: 'm', + HKQuantityTypeIdentifierWaterTemperature: 'degC', + HKQuantityTypeIdentifierWorkoutEffortScore: 'appleEffortScore', +} as const +export const CATEGORY_IDENTIFIER_VALUE_ENUMS = { + HKCategoryTypeIdentifierAbdominalCramps: 'CategoryValueSeverity', + HKCategoryTypeIdentifierAcne: 'CategoryValueSeverity', + HKCategoryTypeIdentifierAppetiteChanges: 'CategoryValueAppetiteChanges', + HKCategoryTypeIdentifierAppleStandHour: 'CategoryValueAppleStandHour', + HKCategoryTypeIdentifierAppleWalkingSteadinessEvent: + 'CategoryValueAppleWalkingSteadinessEvent', + HKCategoryTypeIdentifierAudioExposureEvent: null, + HKCategoryTypeIdentifierBladderIncontinence: 'CategoryValueSeverity', + HKCategoryTypeIdentifierBleedingAfterPregnancy: + 'CategoryValueVaginalBleeding', + HKCategoryTypeIdentifierBleedingDuringPregnancy: + 'CategoryValueVaginalBleeding', + HKCategoryTypeIdentifierBloating: 'CategoryValueSeverity', + HKCategoryTypeIdentifierBreastPain: 'CategoryValueSeverity', + HKCategoryTypeIdentifierCervicalMucusQuality: + 'CategoryValueCervicalMucusQuality', + HKCategoryTypeIdentifierChestTightnessOrPain: 'CategoryValueSeverity', + HKCategoryTypeIdentifierChills: 'CategoryValueSeverity', + HKCategoryTypeIdentifierConstipation: 'CategoryValueSeverity', + HKCategoryTypeIdentifierContraceptive: 'CategoryValueContraceptive', + HKCategoryTypeIdentifierCoughing: 'CategoryValueSeverity', + HKCategoryTypeIdentifierDiarrhea: 'CategoryValueSeverity', + HKCategoryTypeIdentifierDizziness: 'CategoryValueSeverity', + HKCategoryTypeIdentifierDrySkin: 'CategoryValueSeverity', + HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent: + 'CategoryValueEnvironmentalAudioExposureEvent', + HKCategoryTypeIdentifierFainting: 'CategoryValueSeverity', + HKCategoryTypeIdentifierFatigue: 'CategoryValueSeverity', + HKCategoryTypeIdentifierFever: 'CategoryValueSeverity', + HKCategoryTypeIdentifierGeneralizedBodyAche: 'CategoryValueSeverity', + HKCategoryTypeIdentifierHairLoss: 'CategoryValueSeverity', + HKCategoryTypeIdentifierHandwashingEvent: 'CategoryValue', + HKCategoryTypeIdentifierHeadache: 'CategoryValueSeverity', + HKCategoryTypeIdentifierHeadphoneAudioExposureEvent: + 'CategoryValueHeadphoneAudioExposureEvent', + HKCategoryTypeIdentifierHeartburn: 'CategoryValueSeverity', + HKCategoryTypeIdentifierHighHeartRateEvent: 'CategoryValue', + HKCategoryTypeIdentifierHotFlashes: 'CategoryValueSeverity', + HKCategoryTypeIdentifierHypertensionEvent: 'CategoryValue', + HKCategoryTypeIdentifierInfrequentMenstrualCycles: 'CategoryValue', + HKCategoryTypeIdentifierIntermenstrualBleeding: 'CategoryValue', + HKCategoryTypeIdentifierIrregularHeartRhythmEvent: 'CategoryValue', + HKCategoryTypeIdentifierIrregularMenstrualCycles: 'CategoryValue', + HKCategoryTypeIdentifierLactation: 'CategoryValue', + HKCategoryTypeIdentifierLossOfSmell: 'CategoryValueSeverity', + HKCategoryTypeIdentifierLossOfTaste: 'CategoryValueSeverity', + HKCategoryTypeIdentifierLowCardioFitnessEvent: + 'CategoryValueLowCardioFitnessEvent', + HKCategoryTypeIdentifierLowerBackPain: 'CategoryValueSeverity', + HKCategoryTypeIdentifierLowHeartRateEvent: 'CategoryValue', + HKCategoryTypeIdentifierMemoryLapse: 'CategoryValueSeverity', + HKCategoryTypeIdentifierMenstrualFlow: 'CategoryValueMenstrualFlow', + HKCategoryTypeIdentifierMindfulSession: 'CategoryValue', + HKCategoryTypeIdentifierMoodChanges: 'CategoryValuePresence', + HKCategoryTypeIdentifierNausea: 'CategoryValueSeverity', + HKCategoryTypeIdentifierNightSweats: 'CategoryValueSeverity', + HKCategoryTypeIdentifierOvulationTestResult: + 'CategoryValueOvulationTestResult', + HKCategoryTypeIdentifierPelvicPain: 'CategoryValueSeverity', + HKCategoryTypeIdentifierPersistentIntermenstrualBleeding: 'CategoryValue', + HKCategoryTypeIdentifierPregnancy: 'CategoryValue', + HKCategoryTypeIdentifierPregnancyTestResult: + 'CategoryValuePregnancyTestResult', + HKCategoryTypeIdentifierProgesteroneTestResult: + 'CategoryValueProgesteroneTestResult', + HKCategoryTypeIdentifierProlongedMenstrualPeriods: 'CategoryValue', + HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat: + 'CategoryValueSeverity', + HKCategoryTypeIdentifierRunnyNose: 'CategoryValueSeverity', + HKCategoryTypeIdentifierSexualActivity: 'CategoryValue', + HKCategoryTypeIdentifierShortnessOfBreath: 'CategoryValueSeverity', + HKCategoryTypeIdentifierSinusCongestion: 'CategoryValueSeverity', + HKCategoryTypeIdentifierSkippedHeartbeat: 'CategoryValueSeverity', + HKCategoryTypeIdentifierSleepAnalysis: 'CategoryValueSleepAnalysis', + HKCategoryTypeIdentifierSleepApneaEvent: 'CategoryValue', + HKCategoryTypeIdentifierSleepChanges: 'CategoryValuePresence', + HKCategoryTypeIdentifierSoreThroat: 'CategoryValueSeverity', + HKCategoryTypeIdentifierToothbrushingEvent: 'CategoryValue', + HKCategoryTypeIdentifierVaginalDryness: 'CategoryValueSeverity', + HKCategoryTypeIdentifierVomiting: 'CategoryValueSeverity', + HKCategoryTypeIdentifierWheezing: 'CategoryValueSeverity', +} as const +export enum AppleECGAlgorithmVersion { + value1 = 1, + value2 = 2, +} +export enum BloodGlucoseMealTime { + preprandial = 1, + postprandial = 2, +} +export enum BodyTemperatureSensorLocation { + other = 0, + armpit = 1, + body = 2, + ear = 3, + finger = 4, + gastroIntestinal = 5, + mouth = 6, + rectum = 7, + toe = 8, + earDrum = 9, + temporalArtery = 10, + forehead = 11, +} +export enum CategoryValue { + notApplicable = 0, +} +export enum CategoryValueAppetiteChanges { + unspecified = 0, + noChange = 1, + decreased = 2, + increased = 3, +} +export enum CategoryValueAppleStandHour { + stood = 0, + idle = 1, +} +export enum CategoryValueAppleWalkingSteadinessEvent { + initialLow = 1, + initialVeryLow = 2, + repeatLow = 3, + repeatVeryLow = 4, +} +export enum CategoryValueAudioExposureEvent { + loudEnvironment = 1, +} +export enum CategoryValueCervicalMucusQuality { + dry = 1, + sticky = 2, + creamy = 3, + watery = 4, + eggWhite = 5, +} +export enum CategoryValueContraceptive { + unspecified = 1, + implant = 2, + injection = 3, + intrauterineDevice = 4, + intravaginalRing = 5, + oral = 6, + patch = 7, +} +export enum CategoryValueEnvironmentalAudioExposureEvent { + momentaryLimit = 1, +} +export enum CategoryValueHeadphoneAudioExposureEvent { + sevenDayLimit = 1, +} +export enum CategoryValueLowCardioFitnessEvent { + lowFitness = 1, +} +export enum CategoryValueMenstrualFlow { + unspecified = 1, + light = 2, + medium = 3, + heavy = 4, + none = 5, +} +export enum CategoryValueOvulationTestResult { + negative = 1, + luteinizingHormoneSurge = 2, + positive = 2, + indeterminate = 3, + estrogenSurge = 4, +} +export enum CategoryValuePregnancyTestResult { + negative = 1, + positive = 2, + indeterminate = 3, +} +export enum CategoryValuePresence { + present = 0, + notPresent = 1, +} +export enum CategoryValueProgesteroneTestResult { + negative = 1, + positive = 2, + indeterminate = 3, +} +export enum CategoryValueSeverity { + unspecified = 0, + notPresent = 1, + mild = 2, + moderate = 3, + severe = 4, +} +export enum CategoryValueSleepAnalysis { + inBed = 0, + asleepUnspecified = 1, + asleep = 1, + awake = 2, + asleepCore = 3, + asleepDeep = 4, + asleepREM = 5, +} +export enum CategoryValueVaginalBleeding { + unspecified = 1, + light = 2, + medium = 3, + heavy = 4, + none = 5, +} +export enum CyclingFunctionalThresholdPowerTestType { + maxExercise60Minute = 1, + maxExercise20Minute = 2, + rampTest = 3, + predictionExercise = 4, +} +export enum DevicePlacementSide { + unknown = 0, + left = 1, + right = 2, + central = 3, +} +export enum HeartRateMotionContext { + notSet = 0, + sedentary = 1, + active = 2, +} +export enum HeartRateRecoveryTestType { + maxExercise = 1, + predictionSubMaxExercise = 2, + predictionNonExercise = 3, +} +export enum HeartRateSensorLocation { + other = 0, + chest = 1, + wrist = 2, + finger = 3, + hand = 4, + earLobe = 5, + foot = 6, +} +export enum InsulinDeliveryReason { + basal = 1, + bolus = 2, +} +export enum PhysicalEffortEstimationType { + activityLookup = 1, + deviceSensed = 2, +} +export enum SwimmingStrokeStyle { + unknown = 0, + mixed = 1, + freestyle = 2, + backstroke = 3, + breaststroke = 4, + butterfly = 5, + kickboard = 6, +} +export enum UserMotionContext { + notSet = 0, + stationary = 1, + active = 2, +} +export enum VO2MaxTestType { + maxExercise = 1, + predictionSubMaxExercise = 2, + predictionNonExercise = 3, + predictionStepTest = 4, +} +export enum WaterSalinity { + freshWater = 1, + saltWater = 2, +} +export enum WeatherCondition { + none = 0, + clear = 1, + fair = 2, + partlyCloudy = 3, + mostlyCloudy = 4, + cloudy = 5, + foggy = 6, + haze = 7, + windy = 8, + blustery = 9, + smoky = 10, + dust = 11, + snow = 12, + hail = 13, + sleet = 14, + freezingDrizzle = 15, + freezingRain = 16, + mixedRainAndHail = 17, + mixedRainAndSnow = 18, + mixedRainAndSleet = 19, + mixedSnowAndSleet = 20, + drizzle = 21, + scatteredShowers = 22, + showers = 23, + thunderstorms = 24, + tropicalStorm = 25, + hurricane = 26, + tornado = 27, +} +export enum WorkoutActivityType { + americanFootball = 1, + archery = 2, + australianFootball = 3, + badminton = 4, + baseball = 5, + basketball = 6, + bowling = 7, + boxing = 8, + climbing = 9, + cricket = 10, + crossTraining = 11, + curling = 12, + cycling = 13, + dance = 14, + danceInspiredTraining = 15, + elliptical = 16, + equestrianSports = 17, + fencing = 18, + fishing = 19, + functionalStrengthTraining = 20, + golf = 21, + gymnastics = 22, + handball = 23, + hiking = 24, + hockey = 25, + hunting = 26, + lacrosse = 27, + martialArts = 28, + mindAndBody = 29, + mixedMetabolicCardioTraining = 30, + paddleSports = 31, + play = 32, + preparationAndRecovery = 33, + racquetball = 34, + rowing = 35, + rugby = 36, + running = 37, + sailing = 38, + skatingSports = 39, + snowSports = 40, + soccer = 41, + softball = 42, + squash = 43, + stairClimbing = 44, + surfingSports = 45, + swimming = 46, + tableTennis = 47, + tennis = 48, + trackAndField = 49, + traditionalStrengthTraining = 50, + volleyball = 51, + walking = 52, + waterFitness = 53, + waterPolo = 54, + waterSports = 55, + wrestling = 56, + yoga = 57, + barre = 58, + coreTraining = 59, + crossCountrySkiing = 60, + downhillSkiing = 61, + flexibility = 62, + highIntensityIntervalTraining = 63, + jumpRope = 64, + kickboxing = 65, + pilates = 66, + snowboarding = 67, + stairs = 68, + stepTraining = 69, + wheelchairWalkPace = 70, + wheelchairRunPace = 71, + taiChi = 72, + mixedCardio = 73, + handCycling = 74, + discSports = 75, + fitnessGaming = 76, + cardioDance = 77, + socialDance = 78, + pickleball = 79, + cooldown = 80, + swimBikeRun = 82, + transition = 83, + underwaterDiving = 84, + other = 3000, +} +export enum WorkoutEventType { + pause = 1, + resume = 2, + lap = 3, + marker = 4, + motionPaused = 5, + motionResumed = 6, + segment = 7, + pauseOrResumeRequest = 8, +} +export enum WorkoutSwimmingLocationType { + unknown = 0, + pool = 1, + openWater = 2, +} +export interface CategoryValueByIdentifierMap { + readonly HKCategoryTypeIdentifierAbdominalCramps: CategoryValueSeverity + readonly HKCategoryTypeIdentifierAcne: CategoryValueSeverity + readonly HKCategoryTypeIdentifierAppetiteChanges: CategoryValueAppetiteChanges + readonly HKCategoryTypeIdentifierAppleStandHour: CategoryValueAppleStandHour + readonly HKCategoryTypeIdentifierAppleWalkingSteadinessEvent: CategoryValueAppleWalkingSteadinessEvent + readonly HKCategoryTypeIdentifierBladderIncontinence: CategoryValueSeverity + readonly HKCategoryTypeIdentifierBleedingAfterPregnancy: CategoryValueVaginalBleeding + readonly HKCategoryTypeIdentifierBleedingDuringPregnancy: CategoryValueVaginalBleeding + readonly HKCategoryTypeIdentifierBloating: CategoryValueSeverity + readonly HKCategoryTypeIdentifierBreastPain: CategoryValueSeverity + readonly HKCategoryTypeIdentifierCervicalMucusQuality: CategoryValueCervicalMucusQuality + readonly HKCategoryTypeIdentifierChestTightnessOrPain: CategoryValueSeverity + readonly HKCategoryTypeIdentifierChills: CategoryValueSeverity + readonly HKCategoryTypeIdentifierConstipation: CategoryValueSeverity + readonly HKCategoryTypeIdentifierContraceptive: CategoryValueContraceptive + readonly HKCategoryTypeIdentifierCoughing: CategoryValueSeverity + readonly HKCategoryTypeIdentifierDiarrhea: CategoryValueSeverity + readonly HKCategoryTypeIdentifierDizziness: CategoryValueSeverity + readonly HKCategoryTypeIdentifierDrySkin: CategoryValueSeverity + readonly HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent: CategoryValueEnvironmentalAudioExposureEvent + readonly HKCategoryTypeIdentifierFainting: CategoryValueSeverity + readonly HKCategoryTypeIdentifierFatigue: CategoryValueSeverity + readonly HKCategoryTypeIdentifierFever: CategoryValueSeverity + readonly HKCategoryTypeIdentifierGeneralizedBodyAche: CategoryValueSeverity + readonly HKCategoryTypeIdentifierHairLoss: CategoryValueSeverity + readonly HKCategoryTypeIdentifierHandwashingEvent: CategoryValue + readonly HKCategoryTypeIdentifierHeadache: CategoryValueSeverity + readonly HKCategoryTypeIdentifierHeadphoneAudioExposureEvent: CategoryValueHeadphoneAudioExposureEvent + readonly HKCategoryTypeIdentifierHeartburn: CategoryValueSeverity + readonly HKCategoryTypeIdentifierHighHeartRateEvent: CategoryValue + readonly HKCategoryTypeIdentifierHotFlashes: CategoryValueSeverity + readonly HKCategoryTypeIdentifierHypertensionEvent: CategoryValue + readonly HKCategoryTypeIdentifierInfrequentMenstrualCycles: CategoryValue + readonly HKCategoryTypeIdentifierIntermenstrualBleeding: CategoryValue + readonly HKCategoryTypeIdentifierIrregularHeartRhythmEvent: CategoryValue + readonly HKCategoryTypeIdentifierIrregularMenstrualCycles: CategoryValue + readonly HKCategoryTypeIdentifierLactation: CategoryValue + readonly HKCategoryTypeIdentifierLossOfSmell: CategoryValueSeverity + readonly HKCategoryTypeIdentifierLossOfTaste: CategoryValueSeverity + readonly HKCategoryTypeIdentifierLowCardioFitnessEvent: CategoryValueLowCardioFitnessEvent + readonly HKCategoryTypeIdentifierLowerBackPain: CategoryValueSeverity + readonly HKCategoryTypeIdentifierLowHeartRateEvent: CategoryValue + readonly HKCategoryTypeIdentifierMemoryLapse: CategoryValueSeverity + readonly HKCategoryTypeIdentifierMenstrualFlow: CategoryValueMenstrualFlow + readonly HKCategoryTypeIdentifierMindfulSession: CategoryValue + readonly HKCategoryTypeIdentifierMoodChanges: CategoryValuePresence + readonly HKCategoryTypeIdentifierNausea: CategoryValueSeverity + readonly HKCategoryTypeIdentifierNightSweats: CategoryValueSeverity + readonly HKCategoryTypeIdentifierOvulationTestResult: CategoryValueOvulationTestResult + readonly HKCategoryTypeIdentifierPelvicPain: CategoryValueSeverity + readonly HKCategoryTypeIdentifierPersistentIntermenstrualBleeding: CategoryValue + readonly HKCategoryTypeIdentifierPregnancy: CategoryValue + readonly HKCategoryTypeIdentifierPregnancyTestResult: CategoryValuePregnancyTestResult + readonly HKCategoryTypeIdentifierProgesteroneTestResult: CategoryValueProgesteroneTestResult + readonly HKCategoryTypeIdentifierProlongedMenstrualPeriods: CategoryValue + readonly HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat: CategoryValueSeverity + readonly HKCategoryTypeIdentifierRunnyNose: CategoryValueSeverity + readonly HKCategoryTypeIdentifierSexualActivity: CategoryValue + readonly HKCategoryTypeIdentifierShortnessOfBreath: CategoryValueSeverity + readonly HKCategoryTypeIdentifierSinusCongestion: CategoryValueSeverity + readonly HKCategoryTypeIdentifierSkippedHeartbeat: CategoryValueSeverity + readonly HKCategoryTypeIdentifierSleepAnalysis: CategoryValueSleepAnalysis + readonly HKCategoryTypeIdentifierSleepApneaEvent: CategoryValue + readonly HKCategoryTypeIdentifierSleepChanges: CategoryValuePresence + readonly HKCategoryTypeIdentifierSoreThroat: CategoryValueSeverity + readonly HKCategoryTypeIdentifierToothbrushingEvent: CategoryValue + readonly HKCategoryTypeIdentifierVaginalDryness: CategoryValueSeverity + readonly HKCategoryTypeIdentifierVomiting: CategoryValueSeverity + readonly HKCategoryTypeIdentifierWheezing: CategoryValueSeverity +} +export type CategoryValueForIdentifierGenerated< + T extends CategoryTypeIdentifier = CategoryTypeIdentifier, +> = T extends keyof CategoryValueByIdentifierMap + ? CategoryValueByIdentifierMap[T] + : number +export interface KnownObjectMetadata { + readonly HKDeviceManufacturerName?: string + readonly HKDeviceName?: string + readonly HKDeviceSerialNumber?: string + readonly HKDigitalSignature?: string + readonly HKExternalUUID?: string + readonly HKFoodType?: string + readonly HKReferenceRangeLowerLimit?: number + readonly HKReferenceRangeUpperLimit?: number + readonly HKSyncIdentifier?: string + readonly HKSyncVersion?: number + readonly HKTimeZone?: string + readonly HKUDIDeviceIdentifier?: string + readonly HKUDIProductionIdentifier?: string + readonly HKWasTakenInLab?: boolean + readonly HKWasUserEntered?: boolean +} +export interface KnownSampleMetadata extends KnownObjectMetadata { + readonly HKActivityType?: WorkoutActivityType + readonly HKAlgorithmVersion?: number + readonly HKAppleDeviceCalibrated?: boolean + readonly HKAudioExposureDuration?: Quantity + readonly HKBarometricPressure?: Quantity + readonly HKDateOfEarliestDataUsedForEstimate?: string + readonly HKMaximumLightIntensity?: Quantity + readonly HKPhysicalEffortEstimationType?: PhysicalEffortEstimationType + readonly HKUserMotionContext?: UserMotionContext + readonly HKWaterSalinity?: WaterSalinity + readonly HKWeatherCondition?: WeatherCondition + readonly HKWeatherHumidity?: Quantity + readonly HKWeatherTemperature?: Quantity +} +export interface CategoryTypedMetadata extends KnownSampleMetadata { + readonly HKAudioExposureLevel?: Quantity + readonly HKHeadphoneGain?: Quantity + readonly HKHeartRateEventThreshold?: Quantity + readonly HKLowCardioFitnessEventThreshold?: Quantity + readonly HKMenstrualCycleStart?: boolean + readonly HKSexualActivityProtectionUsed?: boolean + readonly HKVO2MaxValue?: Quantity +} +export interface QuantityTypedMetadata extends KnownSampleMetadata { + readonly HKHeartRateMotionContext?: HeartRateMotionContext + readonly HKHeartRateRecoveryActivityDuration?: Quantity + readonly HKHeartRateRecoveryActivityType?: WorkoutActivityType + readonly HKHeartRateRecoveryMaxObservedRecoveryHeartRate?: Quantity + readonly HKHeartRateRecoveryTestType?: HeartRateRecoveryTestType + readonly HKHeartRateSensorLocation?: HeartRateSensorLocation + readonly HKQuantityClampedToLowerBound?: boolean + readonly HKQuantityClampedToUpperBound?: boolean + readonly HKSessionEstimate?: Quantity + readonly HKBloodGlucoseMealTime?: BloodGlucoseMealTime + readonly HKBodyTemperatureSensorLocation?: BodyTemperatureSensorLocation + readonly HKInsulinDeliveryReason?: InsulinDeliveryReason + readonly HKVO2MaxTestType?: VO2MaxTestType +} +export interface WorkoutTypedMetadata extends KnownSampleMetadata { + readonly HKAlpineSlopeGrade?: Quantity + readonly HKAppleFitnessPlusCatalogIdentifier?: string + readonly HKAppleFitnessPlusSession?: boolean + readonly HKAverageMETs?: Quantity + readonly HKAverageSpeed?: Quantity + readonly HKCoachedWorkout?: boolean + readonly HKCrossTrainerDistance?: Quantity + readonly HKElevationAscended?: Quantity + readonly HKElevationDescended?: Quantity + readonly HKFitnessMachineDuration?: Quantity + readonly HKGroupFitness?: boolean + readonly HKIndoorBikeDistance?: Quantity + readonly HKIndoorWorkout?: boolean + readonly HKLapLength?: Quantity + readonly HKMaximumSpeed?: Quantity + readonly HKSwimmingLocationType?: WorkoutSwimmingLocationType + readonly HKSWOLFScore?: number + readonly HKWeatherCondition?: WeatherCondition + readonly HKWeatherHumidity?: Quantity + readonly HKWeatherTemperature?: Quantity + readonly HKWorkoutBrandName?: string +} +export interface WorkoutEventTypedMetadata { + readonly HKSwimmingStrokeStyle?: SwimmingStrokeStyle +} +export interface CategorySpecificMetadataByIdentifierMap { + readonly HKCategoryTypeIdentifierAudioExposureEvent: Pick< + CategoryTypedMetadata, + 'HKAudioExposureLevel' | 'HKHeadphoneGain' + > + readonly HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent: Pick< + CategoryTypedMetadata, + 'HKAudioExposureLevel' + > + readonly HKCategoryTypeIdentifierHeadphoneAudioExposureEvent: Pick< + CategoryTypedMetadata, + 'HKAudioExposureLevel' | 'HKHeadphoneGain' + > + readonly HKCategoryTypeIdentifierHighHeartRateEvent: Pick< + CategoryTypedMetadata, + 'HKHeartRateEventThreshold' + > + readonly HKCategoryTypeIdentifierLowCardioFitnessEvent: Pick< + CategoryTypedMetadata, + 'HKLowCardioFitnessEventThreshold' | 'HKVO2MaxValue' + > + readonly HKCategoryTypeIdentifierLowHeartRateEvent: Pick< + CategoryTypedMetadata, + 'HKHeartRateEventThreshold' + > + readonly HKCategoryTypeIdentifierMenstrualFlow: Pick< + CategoryTypedMetadata, + 'HKMenstrualCycleStart' + > + readonly HKCategoryTypeIdentifierSexualActivity: Pick< + CategoryTypedMetadata, + 'HKSexualActivityProtectionUsed' + > +} +export type CategoryTypedMetadataForIdentifierGenerated< + T extends CategoryTypeIdentifier = CategoryTypeIdentifier, +> = KnownSampleMetadata & + (T extends keyof CategorySpecificMetadataByIdentifierMap + ? CategorySpecificMetadataByIdentifierMap[T] + : Record) +export interface QuantitySpecificMetadataByIdentifierMap { + readonly HKQuantityTypeIdentifierBasalBodyTemperature: Pick< + QuantityTypedMetadata, + 'HKBodyTemperatureSensorLocation' + > + readonly HKQuantityTypeIdentifierBloodGlucose: Pick< + QuantityTypedMetadata, + 'HKBloodGlucoseMealTime' + > + readonly HKQuantityTypeIdentifierBodyTemperature: Pick< + QuantityTypedMetadata, + 'HKBodyTemperatureSensorLocation' + > + readonly HKQuantityTypeIdentifierInsulinDelivery: Pick< + QuantityTypedMetadata, + 'HKInsulinDeliveryReason' + > + readonly HKQuantityTypeIdentifierVO2Max: Pick< + QuantityTypedMetadata, + 'HKVO2MaxTestType' + > +} +export type QuantityTypedMetadataForIdentifierGenerated< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> = KnownSampleMetadata & + Pick< + QuantityTypedMetadata, + | 'HKHeartRateMotionContext' + | 'HKHeartRateRecoveryActivityDuration' + | 'HKHeartRateRecoveryActivityType' + | 'HKHeartRateRecoveryMaxObservedRecoveryHeartRate' + | 'HKHeartRateRecoveryTestType' + | 'HKHeartRateSensorLocation' + | 'HKQuantityClampedToLowerBound' + | 'HKQuantityClampedToUpperBound' + | 'HKSessionEstimate' + > & + (T extends keyof QuantitySpecificMetadataByIdentifierMap + ? QuantitySpecificMetadataByIdentifierMap[T] + : Record) +export interface QuantityUnitByIdentifierMap { + readonly HKQuantityTypeIdentifierActiveEnergyBurned: EnergyUnit + readonly HKQuantityTypeIdentifierAppleExerciseTime: TimeUnit + readonly HKQuantityTypeIdentifierAppleMoveTime: TimeUnit + readonly HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances: 'count' + readonly HKQuantityTypeIdentifierAppleSleepingWristTemperature: TemperatureUnit + readonly HKQuantityTypeIdentifierAppleStandTime: TimeUnit + readonly HKQuantityTypeIdentifierAppleWalkingSteadiness: '%' + readonly HKQuantityTypeIdentifierAtrialFibrillationBurden: '%' + readonly HKQuantityTypeIdentifierBasalBodyTemperature: TemperatureUnit + readonly HKQuantityTypeIdentifierBasalEnergyBurned: EnergyUnit + readonly HKQuantityTypeIdentifierBloodAlcoholContent: '%' + readonly HKQuantityTypeIdentifierBloodGlucose: BloodGlucoseUnit + readonly HKQuantityTypeIdentifierBloodPressureDiastolic: PressureUnit + readonly HKQuantityTypeIdentifierBloodPressureSystolic: PressureUnit + readonly HKQuantityTypeIdentifierBodyFatPercentage: '%' + readonly HKQuantityTypeIdentifierBodyMass: MassUnit + readonly HKQuantityTypeIdentifierBodyMassIndex: 'count' + readonly HKQuantityTypeIdentifierBodyTemperature: TemperatureUnit + readonly HKQuantityTypeIdentifierCrossCountrySkiingSpeed: SpeedUnit< + LengthUnit, + TimeUnit + > + readonly HKQuantityTypeIdentifierCyclingCadence: CountPerTime + readonly HKQuantityTypeIdentifierCyclingFunctionalThresholdPower: string + readonly HKQuantityTypeIdentifierCyclingPower: string + readonly HKQuantityTypeIdentifierCyclingSpeed: SpeedUnit + readonly HKQuantityTypeIdentifierDietaryBiotin: MassUnit + readonly HKQuantityTypeIdentifierDietaryCaffeine: MassUnit + readonly HKQuantityTypeIdentifierDietaryCalcium: MassUnit + readonly HKQuantityTypeIdentifierDietaryCarbohydrates: MassUnit + readonly HKQuantityTypeIdentifierDietaryChloride: MassUnit + readonly HKQuantityTypeIdentifierDietaryCholesterol: MassUnit + readonly HKQuantityTypeIdentifierDietaryChromium: MassUnit + readonly HKQuantityTypeIdentifierDietaryCopper: MassUnit + readonly HKQuantityTypeIdentifierDietaryEnergyConsumed: EnergyUnit + readonly HKQuantityTypeIdentifierDietaryFatMonounsaturated: MassUnit + readonly HKQuantityTypeIdentifierDietaryFatPolyunsaturated: MassUnit + readonly HKQuantityTypeIdentifierDietaryFatSaturated: MassUnit + readonly HKQuantityTypeIdentifierDietaryFatTotal: MassUnit + readonly HKQuantityTypeIdentifierDietaryFiber: MassUnit + readonly HKQuantityTypeIdentifierDietaryFolate: MassUnit + readonly HKQuantityTypeIdentifierDietaryIodine: MassUnit + readonly HKQuantityTypeIdentifierDietaryIron: MassUnit + readonly HKQuantityTypeIdentifierDietaryMagnesium: MassUnit + readonly HKQuantityTypeIdentifierDietaryManganese: MassUnit + readonly HKQuantityTypeIdentifierDietaryMolybdenum: MassUnit + readonly HKQuantityTypeIdentifierDietaryNiacin: MassUnit + readonly HKQuantityTypeIdentifierDietaryPantothenicAcid: MassUnit + readonly HKQuantityTypeIdentifierDietaryPhosphorus: MassUnit + readonly HKQuantityTypeIdentifierDietaryPotassium: MassUnit + readonly HKQuantityTypeIdentifierDietaryProtein: MassUnit + readonly HKQuantityTypeIdentifierDietaryRiboflavin: MassUnit + readonly HKQuantityTypeIdentifierDietarySelenium: MassUnit + readonly HKQuantityTypeIdentifierDietarySodium: MassUnit + readonly HKQuantityTypeIdentifierDietarySugar: MassUnit + readonly HKQuantityTypeIdentifierDietaryThiamin: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminA: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminB12: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminB6: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminC: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminD: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminE: MassUnit + readonly HKQuantityTypeIdentifierDietaryVitaminK: MassUnit + readonly HKQuantityTypeIdentifierDietaryWater: string + readonly HKQuantityTypeIdentifierDietaryZinc: MassUnit + readonly HKQuantityTypeIdentifierDistanceCrossCountrySkiing: LengthUnit + readonly HKQuantityTypeIdentifierDistanceCycling: LengthUnit + readonly HKQuantityTypeIdentifierDistanceDownhillSnowSports: LengthUnit + readonly HKQuantityTypeIdentifierDistancePaddleSports: LengthUnit + readonly HKQuantityTypeIdentifierDistanceRowing: LengthUnit + readonly HKQuantityTypeIdentifierDistanceSkatingSports: LengthUnit + readonly HKQuantityTypeIdentifierDistanceSwimming: LengthUnit + readonly HKQuantityTypeIdentifierDistanceWalkingRunning: LengthUnit + readonly HKQuantityTypeIdentifierDistanceWheelchair: LengthUnit + readonly HKQuantityTypeIdentifierElectrodermalActivity: string + readonly HKQuantityTypeIdentifierEnvironmentalAudioExposure: PressureUnit + readonly HKQuantityTypeIdentifierEnvironmentalSoundReduction: PressureUnit + readonly HKQuantityTypeIdentifierEstimatedWorkoutEffortScore: 'appleEffortScore' + readonly HKQuantityTypeIdentifierFlightsClimbed: 'count' + readonly HKQuantityTypeIdentifierForcedExpiratoryVolume1: string + readonly HKQuantityTypeIdentifierForcedVitalCapacity: string + readonly HKQuantityTypeIdentifierHeadphoneAudioExposure: PressureUnit + readonly HKQuantityTypeIdentifierHeartRate: CountPerTime + readonly HKQuantityTypeIdentifierHeartRateRecoveryOneMinute: CountPerTime + readonly HKQuantityTypeIdentifierHeartRateVariabilitySDNN: string + readonly HKQuantityTypeIdentifierHeight: LengthUnit + readonly HKQuantityTypeIdentifierInhalerUsage: 'count' + readonly HKQuantityTypeIdentifierInsulinDelivery: 'IU' + readonly HKQuantityTypeIdentifierLeanBodyMass: MassUnit + readonly HKQuantityTypeIdentifierNikeFuel: 'count' + readonly HKQuantityTypeIdentifierNumberOfAlcoholicBeverages: 'count' + readonly HKQuantityTypeIdentifierNumberOfTimesFallen: 'count' + readonly HKQuantityTypeIdentifierOxygenSaturation: '%' + readonly HKQuantityTypeIdentifierPaddleSportsSpeed: SpeedUnit< + LengthUnit, + TimeUnit + > + readonly HKQuantityTypeIdentifierPeakExpiratoryFlowRate: string + readonly HKQuantityTypeIdentifierPeripheralPerfusionIndex: '%' + readonly HKQuantityTypeIdentifierPhysicalEffort: string + readonly HKQuantityTypeIdentifierPushCount: 'count' + readonly HKQuantityTypeIdentifierRespiratoryRate: CountPerTime + readonly HKQuantityTypeIdentifierRestingHeartRate: CountPerTime + readonly HKQuantityTypeIdentifierRowingSpeed: SpeedUnit + readonly HKQuantityTypeIdentifierRunningGroundContactTime: string + readonly HKQuantityTypeIdentifierRunningPower: string + readonly HKQuantityTypeIdentifierRunningSpeed: SpeedUnit + readonly HKQuantityTypeIdentifierRunningStrideLength: LengthUnit + readonly HKQuantityTypeIdentifierRunningVerticalOscillation: LengthUnit + readonly HKQuantityTypeIdentifierSixMinuteWalkTestDistance: LengthUnit + readonly HKQuantityTypeIdentifierStairAscentSpeed: SpeedUnit< + LengthUnit, + TimeUnit + > + readonly HKQuantityTypeIdentifierStairDescentSpeed: SpeedUnit< + LengthUnit, + TimeUnit + > + readonly HKQuantityTypeIdentifierStepCount: 'count' + readonly HKQuantityTypeIdentifierSwimmingStrokeCount: 'count' + readonly HKQuantityTypeIdentifierTimeInDaylight: TimeUnit + readonly HKQuantityTypeIdentifierUnderwaterDepth: LengthUnit + readonly HKQuantityTypeIdentifierUVExposure: string + readonly HKQuantityTypeIdentifierVO2Max: string + readonly HKQuantityTypeIdentifierWaistCircumference: LengthUnit + readonly HKQuantityTypeIdentifierWalkingAsymmetryPercentage: '%' + readonly HKQuantityTypeIdentifierWalkingDoubleSupportPercentage: '%' + readonly HKQuantityTypeIdentifierWalkingHeartRateAverage: CountPerTime + readonly HKQuantityTypeIdentifierWalkingSpeed: SpeedUnit + readonly HKQuantityTypeIdentifierWalkingStepLength: LengthUnit + readonly HKQuantityTypeIdentifierWaterTemperature: TemperatureUnit + readonly HKQuantityTypeIdentifierWorkoutEffortScore: 'appleEffortScore' +} +export type UnitForIdentifierGenerated< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> = T extends keyof QuantityUnitByIdentifierMap + ? QuantityUnitByIdentifierMap[T] + : string diff --git a/packages/react-native-healthkit/src/healthkit.ios.ts b/packages/react-native-healthkit/src/healthkit.ios.ts index 41ec90e4..5e1d92d2 100644 --- a/packages/react-native-healthkit/src/healthkit.ios.ts +++ b/packages/react-native-healthkit/src/healthkit.ios.ts @@ -21,7 +21,31 @@ import { StateOfMind, Workouts, } from './modules' +import type { + CorrelationSampleTyped, + QueryCorrelationSamplesWithAnchorResponseTyped, +} from './types/CorrelationType' +import type { + ElectrocardiogramSamplesWithAnchorResponseTyped, + ElectrocardiogramSampleTyped, +} from './types/ElectrocardiogramSample' +import type { + HeartbeatSeriesSamplesWithAnchorResponseTyped, + HeartbeatSeriesSampleTyped, +} from './types/HeartbeatSeries' +import type { + MedicationDoseEventsWithAnchorResponseTyped, + MedicationDoseEventTyped, +} from './types/Medication' import type { QuantityTypeIdentifier } from './types/QuantityTypeIdentifier' +import type { + StateOfMindSamplesWithAnchorResponseTyped, + StateOfMindSampleTyped, +} from './types/StateOfMind' +import type { + QueryWorkoutSamplesWithAnchorResponseTyped, + WorkoutProxyTyped, +} from './types/Workouts' import getMostRecentCategorySample from './utils/getMostRecentCategorySample' import getMostRecentQuantitySample from './utils/getMostRecentQuantitySample' import getMostRecentWorkout from './utils/getMostRecentWorkout' @@ -90,6 +114,188 @@ export type AvailableQuantityTypes< ? AvailableQuantityTypesIOS17Plus : AvailableQuantityTypesBeforeIOS17 +type BoundMethod< + TMethod, + TReturn = TMethod extends (...args: infer _Args) => infer TResult + ? TResult + : never, +> = ( + ...args: TMethod extends (...args: infer TArgs) => unknown ? TArgs : never +) => TReturn + +function bindRetypedMethod( + module: TModule, + method: TMethod, +): BoundMethod { + return ( + method as TMethod extends (...args: infer TArgs) => infer TResult + ? (...args: TArgs) => TResult + : never + ).bind(module) as BoundMethod +} + +const CorrelationTypeBindings = { + queryCorrelationSamples: bindRetypedMethod< + typeof CorrelationTypes, + typeof CorrelationTypes.queryCorrelationSamples, + Promise + >(CorrelationTypes, CorrelationTypes.queryCorrelationSamples), + queryCorrelationSamplesWithAnchor: bindRetypedMethod< + typeof CorrelationTypes, + typeof CorrelationTypes.queryCorrelationSamplesWithAnchor, + Promise + >(CorrelationTypes, CorrelationTypes.queryCorrelationSamplesWithAnchor), + saveCorrelationSample: bindRetypedMethod< + typeof CorrelationTypes, + typeof CorrelationTypes.saveCorrelationSample, + Promise + >(CorrelationTypes, CorrelationTypes.saveCorrelationSample), +} satisfies { + queryCorrelationSamples: BoundMethod< + typeof CorrelationTypes.queryCorrelationSamples, + Promise + > + queryCorrelationSamplesWithAnchor: BoundMethod< + typeof CorrelationTypes.queryCorrelationSamplesWithAnchor, + Promise + > + saveCorrelationSample: BoundMethod< + typeof CorrelationTypes.saveCorrelationSample, + Promise + > +} + +const HeartbeatSeriesBindings = { + queryHeartbeatSeriesSamples: bindRetypedMethod< + typeof HeartbeatSeries, + typeof HeartbeatSeries.queryHeartbeatSeriesSamples, + Promise + >(HeartbeatSeries, HeartbeatSeries.queryHeartbeatSeriesSamples), + queryHeartbeatSeriesSamplesWithAnchor: bindRetypedMethod< + typeof HeartbeatSeries, + typeof HeartbeatSeries.queryHeartbeatSeriesSamplesWithAnchor, + Promise + >(HeartbeatSeries, HeartbeatSeries.queryHeartbeatSeriesSamplesWithAnchor), +} satisfies { + queryHeartbeatSeriesSamples: BoundMethod< + typeof HeartbeatSeries.queryHeartbeatSeriesSamples, + Promise + > + queryHeartbeatSeriesSamplesWithAnchor: BoundMethod< + typeof HeartbeatSeries.queryHeartbeatSeriesSamplesWithAnchor, + Promise + > +} + +const ElectrocardiogramBindings = { + queryElectrocardiogramSamples: bindRetypedMethod< + typeof Electrocardiograms, + typeof Electrocardiograms.queryElectrocardiogramSamples, + Promise + >(Electrocardiograms, Electrocardiograms.queryElectrocardiogramSamples), + queryElectrocardiogramSamplesWithAnchor: bindRetypedMethod< + typeof Electrocardiograms, + typeof Electrocardiograms.queryElectrocardiogramSamplesWithAnchor, + Promise + >( + Electrocardiograms, + Electrocardiograms.queryElectrocardiogramSamplesWithAnchor, + ), +} satisfies { + queryElectrocardiogramSamples: BoundMethod< + typeof Electrocardiograms.queryElectrocardiogramSamples, + Promise + > + queryElectrocardiogramSamplesWithAnchor: BoundMethod< + typeof Electrocardiograms.queryElectrocardiogramSamplesWithAnchor, + Promise + > +} + +const WorkoutBindings = { + queryWorkoutSamples: bindRetypedMethod< + typeof Workouts, + typeof Workouts.queryWorkoutSamples, + Promise + >(Workouts, Workouts.queryWorkoutSamples), + queryWorkoutSamplesWithAnchor: bindRetypedMethod< + typeof Workouts, + typeof Workouts.queryWorkoutSamplesWithAnchor, + Promise + >(Workouts, Workouts.queryWorkoutSamplesWithAnchor), + saveWorkoutSample: bindRetypedMethod< + typeof Workouts, + typeof Workouts.saveWorkoutSample, + Promise + >(Workouts, Workouts.saveWorkoutSample), +} satisfies { + queryWorkoutSamples: BoundMethod< + typeof Workouts.queryWorkoutSamples, + Promise + > + queryWorkoutSamplesWithAnchor: BoundMethod< + typeof Workouts.queryWorkoutSamplesWithAnchor, + Promise + > + saveWorkoutSample: BoundMethod< + typeof Workouts.saveWorkoutSample, + Promise + > +} + +const StateOfMindBindings = { + queryStateOfMindSamples: bindRetypedMethod< + typeof StateOfMind, + typeof StateOfMind.queryStateOfMindSamples, + Promise + >(StateOfMind, StateOfMind.queryStateOfMindSamples), + queryStateOfMindSamplesWithAnchor: bindRetypedMethod< + typeof StateOfMind, + typeof StateOfMind.queryStateOfMindSamplesWithAnchor, + Promise + >(StateOfMind, StateOfMind.queryStateOfMindSamplesWithAnchor), + saveStateOfMindSample: bindRetypedMethod< + typeof StateOfMind, + typeof StateOfMind.saveStateOfMindSample, + Promise + >(StateOfMind, StateOfMind.saveStateOfMindSample), +} satisfies { + queryStateOfMindSamples: BoundMethod< + typeof StateOfMind.queryStateOfMindSamples, + Promise + > + queryStateOfMindSamplesWithAnchor: BoundMethod< + typeof StateOfMind.queryStateOfMindSamplesWithAnchor, + Promise + > + saveStateOfMindSample: BoundMethod< + typeof StateOfMind.saveStateOfMindSample, + Promise + > +} + +const MedicationBindings = { + queryMedicationEvents: bindRetypedMethod< + typeof Medication, + typeof Medication.queryMedicationEvents, + Promise + >(Medication, Medication.queryMedicationEvents), + queryMedicationEventsWithAnchor: bindRetypedMethod< + typeof Medication, + typeof Medication.queryMedicationEventsWithAnchor, + Promise + >(Medication, Medication.queryMedicationEventsWithAnchor), +} satisfies { + queryMedicationEvents: BoundMethod< + typeof Medication.queryMedicationEvents, + Promise + > + queryMedicationEventsWithAnchor: BoundMethod< + typeof Medication.queryMedicationEventsWithAnchor, + Promise + > +} + // Named exports - all functions bound to their respective modules export const authorizationStatusFor = Core.authorizationStatusFor.bind(Core) export const requestPerObjectReadAuthorization = @@ -119,19 +325,17 @@ export const queryCategorySamples = export const queryCategorySamplesWithAnchor = CategoryTypes.queryCategorySamplesWithAnchor.bind(CategoryTypes) export const queryCorrelationSamples = - CorrelationTypes.queryCorrelationSamples.bind(CorrelationTypes) + CorrelationTypeBindings.queryCorrelationSamples export const queryCorrelationSamplesWithAnchor = - CorrelationTypes.queryCorrelationSamplesWithAnchor.bind(CorrelationTypes) + CorrelationTypeBindings.queryCorrelationSamplesWithAnchor export const queryHeartbeatSeriesSamples = - HeartbeatSeries.queryHeartbeatSeriesSamples.bind(HeartbeatSeries) + HeartbeatSeriesBindings.queryHeartbeatSeriesSamples export const queryHeartbeatSeriesSamplesWithAnchor = - HeartbeatSeries.queryHeartbeatSeriesSamplesWithAnchor.bind(HeartbeatSeries) + HeartbeatSeriesBindings.queryHeartbeatSeriesSamplesWithAnchor export const queryElectrocardiogramSamples = - Electrocardiograms.queryElectrocardiogramSamples.bind(Electrocardiograms) + ElectrocardiogramBindings.queryElectrocardiogramSamples export const queryElectrocardiogramSamplesWithAnchor = - Electrocardiograms.queryElectrocardiogramSamplesWithAnchor.bind( - Electrocardiograms, - ) + ElectrocardiogramBindings.queryElectrocardiogramSamplesWithAnchor export const queryQuantitySamples = QuantityTypes.queryQuantitySamples.bind(QuantityTypes) export const queryQuantitySamplesWithAnchor = @@ -146,28 +350,27 @@ export const queryStatisticsCollectionForQuantitySeparateBySource = QuantityTypes.queryStatisticsCollectionForQuantitySeparateBySource.bind( QuantityTypes, ) -export const queryWorkoutSamples = Workouts.queryWorkoutSamples.bind(Workouts) +export const queryWorkoutSamples = WorkoutBindings.queryWorkoutSamples export const queryWorkoutSamplesWithAnchor = - Workouts.queryWorkoutSamplesWithAnchor.bind(Workouts) + WorkoutBindings.queryWorkoutSamplesWithAnchor export const querySources = Core.querySources.bind(Core) export const requestAuthorization = Core.requestAuthorization.bind(Core) export const deleteObjects = Core.deleteObjects.bind(Core) export const saveCategorySample = CategoryTypes.saveCategorySample.bind(CategoryTypes) export const saveCorrelationSample = - CorrelationTypes.saveCorrelationSample.bind(CorrelationTypes) + CorrelationTypeBindings.saveCorrelationSample export const saveQuantitySample = QuantityTypes.saveQuantitySample.bind(QuantityTypes) -export const saveWorkoutSample = Workouts.saveWorkoutSample.bind(Workouts) +export const saveWorkoutSample = WorkoutBindings.saveWorkoutSample export const startWatchApp = Workouts.startWatchAppWithWorkoutConfiguration.bind(Workouts) export const isProtectedDataAvailable = Core.isProtectedDataAvailable.bind(Core) export const queryStateOfMindSamples = - StateOfMind.queryStateOfMindSamples.bind(StateOfMind) + StateOfMindBindings.queryStateOfMindSamples export const queryStateOfMindSamplesWithAnchor = - StateOfMind.queryStateOfMindSamplesWithAnchor.bind(StateOfMind) -export const saveStateOfMindSample = - StateOfMind.saveStateOfMindSample.bind(StateOfMind) + StateOfMindBindings.queryStateOfMindSamplesWithAnchor +export const saveStateOfMindSample = StateOfMindBindings.saveStateOfMindSample export const isQuantityCompatibleWithUnit = QuantityTypes.isQuantityCompatibleWithUnit.bind(QuantityTypes) @@ -181,10 +384,9 @@ export const requestMedicationsAuthorization = Medication.requestMedicationsAuthorization.bind(Medication) export const queryMedications = Medication.queryMedications.bind(Medication) -export const queryMedicationEvents = - Medication.queryMedicationEvents.bind(Medication) +export const queryMedicationEvents = MedicationBindings.queryMedicationEvents export const queryMedicationEventsWithAnchor = - Medication.queryMedicationEventsWithAnchor.bind(Medication) + MedicationBindings.queryMedicationEventsWithAnchor export const currentAppSource = Core.currentAppSource.bind(Core) diff --git a/packages/react-native-healthkit/src/healthkit.ts b/packages/react-native-healthkit/src/healthkit.ts index df2dbc06..73eab32e 100644 --- a/packages/react-native-healthkit/src/healthkit.ts +++ b/packages/react-native-healthkit/src/healthkit.ts @@ -13,18 +13,41 @@ import { AuthorizationRequestStatus, AuthorizationStatus } from './types/Auth' import type { CategorySamplesWithAnchorResponseTyped, CategorySampleTyped, + CategoryValueForIdentifier, + MetadataForCategoryIdentifier, } from './types/CategoryType' -import type { CategoryTypeIdentifier } from './types/CategoryTypeIdentifier' +import type { + CategoryTypeIdentifier, + CategoryTypeIdentifierWriteable, +} from './types/CategoryTypeIdentifier' import { BiologicalSex, BloodType, FitzpatrickSkinType, WheelchairUse, } from './types/Characteristics' -import type { QuantitySample } from './types/QuantitySample' +import type { + MetadataForQuantityIdentifier, + QuantitySampleTyped, +} from './types/QuantitySample' +import type { + IntervalComponents, + QuantitySamplesWithAnchorResponseTyped, + QueryStatisticsResponse, + QueryStatisticsResponseFromSingleSource, + StatisticsOptions, + StatisticsQueryOptions, + UnitForIdentifier, +} from './types/QuantityType' +import type { + QuantityTypeIdentifier, + QuantityTypeIdentifierWriteable, +} from './types/QuantityTypeIdentifier' import type { QueryOptionsWithAnchor, + QueryOptionsWithAnchorAndUnit, QueryOptionsWithSortOrder, + QueryOptionsWithSortOrderAndUnit, } from './types/QueryOptions' export * from './types' @@ -146,47 +169,114 @@ export const getWheelchairUse = UnavailableFnFromModule( ) // QuantityTypeModule functions -export const queryQuantitySamples = UnavailableFnFromModule( - 'queryQuantitySamples', - Promise.resolve([]), -) -export const queryQuantitySamplesWithAnchor = UnavailableFnFromModule( - 'queryQuantitySamplesWithAnchor', - Promise.resolve({ +export function queryQuantitySamples( + _identifier: T, + _options: QueryOptionsWithSortOrderAndUnit>, +): Promise[]> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve([]) +} + +export function queryQuantitySamplesWithAnchor< + T extends QuantityTypeIdentifier, +>( + _identifier: T, + _options: QueryOptionsWithAnchorAndUnit>, +): Promise> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve({ samples: [], deletedSamples: [], newAnchor: '', - }), -) -export const queryStatisticsForQuantity = UnavailableFnFromModule( - 'queryStatisticsForQuantity', - Promise.resolve({ + }) +} +export function queryStatisticsForQuantity( + _identifier: T, + _statistics: readonly StatisticsOptions[], + _options?: StatisticsQueryOptions>, +): Promise { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve({ sources: [], - }), -) -export const queryStatisticsCollectionForQuantity = UnavailableFnFromModule( - 'queryStatisticsCollectionForQuantity', - Promise.resolve([]), -) + }) +} +export function queryStatisticsCollectionForQuantity< + T extends QuantityTypeIdentifier, +>( + _identifier: T, + _statistics: readonly StatisticsOptions[], + _anchorDate: Date, + _intervalComponents: IntervalComponents, + _options?: StatisticsQueryOptions>, +): Promise { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve([]) +} -export const queryStatisticsForQuantitySeparateBySource = - UnavailableFnFromModule( - 'queryStatisticsForQuantitySeparateBySource', - Promise.resolve([]), - ) -export const queryStatisticsCollectionForQuantitySeparateBySource = - UnavailableFnFromModule( - 'queryStatisticsCollectionForQuantitySeparateBySource', - Promise.resolve([]), - ) -export const saveQuantitySample = UnavailableFnFromModule( - 'saveQuantitySample', - Promise.resolve(undefined), -) -export const isQuantityCompatibleWithUnit = UnavailableFnFromModule( - 'isQuantityCompatibleWithUnit', - false, -) +export function queryStatisticsForQuantitySeparateBySource< + T extends QuantityTypeIdentifier, +>( + _identifier: T, + _statistics: readonly StatisticsOptions[], + _options?: StatisticsQueryOptions>, +): Promise { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve([]) +} +export function queryStatisticsCollectionForQuantitySeparateBySource< + T extends QuantityTypeIdentifier, +>( + _identifier: T, + _statistics: readonly StatisticsOptions[], + _anchorDate: Date, + _intervalComponents: IntervalComponents, + _options?: StatisticsQueryOptions>, +): Promise { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve([]) +} +export function saveQuantitySample( + _identifier: T, + _unit: UnitForIdentifier, + _value: number, + _start: Date, + _end: Date, + _metadata?: MetadataForQuantityIdentifier, +): Promise | undefined> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve(undefined) +} +export function isQuantityCompatibleWithUnit( + _identifier: T, + _unit: UnitForIdentifier, +): boolean { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return false +} // CategoryTypeModule functions export function queryCategorySamples( @@ -216,10 +306,20 @@ export function queryCategorySamplesWithAnchor< newAnchor: '', }) } -export const saveCategorySample = UnavailableFnFromModule( - 'saveCategorySample', - Promise.resolve(undefined), -) + +export function saveCategorySample( + _identifier: T, + _value: CategoryValueForIdentifier, + _startDate: Date, + _endDate: Date, + _metadata?: MetadataForCategoryIdentifier, +): Promise | undefined> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve(undefined) +} // CorrelationTypeModule functions export const queryCorrelationSamples = UnavailableFnFromModule( @@ -321,20 +421,30 @@ export function getMostRecentCategorySample( return Promise.resolve(undefined) } -export const getMostRecentQuantitySample = UnavailableFnFromModule( - 'getMostRecentQuantitySample', - // biome-ignore lint/suspicious/noExplicitAny: it works - Promise.resolve(undefined as any as QuantitySample), -) +export function getMostRecentQuantitySample( + _identifier: T, + _unit?: UnitForIdentifier, +): Promise | undefined> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve(undefined) +} export const getMostRecentWorkout = UnavailableFnFromModule( 'getMostRecentWorkout', // biome-ignore lint/suspicious/noExplicitAny: it works Promise.resolve(undefined as any as WorkoutProxy), ) -export const getPreferredUnit = UnavailableFnFromModule( - 'getPreferredUnit', - Promise.resolve('count'), -) // Defaulting to 'count' +export function getPreferredUnit( + _quantityType: T, +): Promise> { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return Promise.resolve('count' as UnitForIdentifier) +} // Hooks (from original export list) export function useMostRecentCategorySample( @@ -347,10 +457,16 @@ export function useMostRecentCategorySample( return undefined } -export const useMostRecentQuantitySample = UnavailableFnFromModule( - 'useMostRecentQuantitySample', - undefined, -) +export function useMostRecentQuantitySample( + _identifier: T, + _unit?: UnitForIdentifier, +): QuantitySampleTyped | undefined { + if (Platform.OS !== 'ios' && !hasWarned) { + console.warn(notAvailableError) + hasWarned = true + } + return undefined +} export const useMostRecentWorkout = UnavailableFnFromModule( 'useMostRecentWorkout', undefined, diff --git a/packages/react-native-healthkit/src/hooks/useMostRecentQuantitySample.ts b/packages/react-native-healthkit/src/hooks/useMostRecentQuantitySample.ts index 66a366b2..4717dbcb 100644 --- a/packages/react-native-healthkit/src/hooks/useMostRecentQuantitySample.ts +++ b/packages/react-native-healthkit/src/hooks/useMostRecentQuantitySample.ts @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react' -import type { QuantitySample } from '../types/QuantitySample' +import type { QuantitySampleTyped } from '../types/QuantitySample' +import type { UnitForIdentifier } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' import getMostRecentQuantitySample from '../utils/getMostRecentQuantitySample' import useSubscribeToChanges from './useSubscribeToChanges' @@ -7,11 +8,11 @@ import useSubscribeToChanges from './useSubscribeToChanges' /** * @returns the most recent sample for the given quantity type. */ -export function useMostRecentQuantitySample( - identifier: QuantityTypeIdentifier, - unit?: string, +export function useMostRecentQuantitySample( + identifier: T, + unit?: UnitForIdentifier, ) { - const [lastSample, setLastSample] = useState() + const [lastSample, setLastSample] = useState>() const fetchMostRecentSample = useCallback(async () => { const value = await getMostRecentQuantitySample(identifier, unit) diff --git a/packages/react-native-healthkit/src/hooks/useMostRecentWorkout.ts b/packages/react-native-healthkit/src/hooks/useMostRecentWorkout.ts index 66686790..78253531 100644 --- a/packages/react-native-healthkit/src/hooks/useMostRecentWorkout.ts +++ b/packages/react-native-healthkit/src/hooks/useMostRecentWorkout.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import type { WorkoutProxy } from '../specs/WorkoutProxy.nitro' +import type { WorkoutProxyTyped } from '../types/Workouts' import getMostRecentWorkout from '../utils/getMostRecentWorkout' import useSubscribeToChanges from './useSubscribeToChanges' @@ -7,7 +7,7 @@ import useSubscribeToChanges from './useSubscribeToChanges' * @returns the most recent workout sample. */ export function useMostRecentWorkout() { - const [workout, setWorkout] = useState() + const [workout, setWorkout] = useState() const update = useCallback(async () => { setWorkout(await getMostRecentWorkout()) diff --git a/packages/react-native-healthkit/src/hooks/useQuantitySampleById.ts b/packages/react-native-healthkit/src/hooks/useQuantitySampleById.ts index da0b998b..b460edd3 100644 --- a/packages/react-native-healthkit/src/hooks/useQuantitySampleById.ts +++ b/packages/react-native-healthkit/src/hooks/useQuantitySampleById.ts @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react' -import type { QuantitySample } from '../types/QuantitySample' +import type { QuantitySampleTyped } from '../types/QuantitySample' +import type { UnitForIdentifier } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' import getQuantitySampleById from '../utils/getQuantitySampleById' import useSubscribeToChanges from './useSubscribeToChanges' @@ -7,15 +8,15 @@ import useSubscribeToChanges from './useSubscribeToChanges' /** * @returns the most recent sample for the given quantity type. */ -export function useQuantitySampleById( - identifier: QuantityTypeIdentifier, +export function useQuantitySampleById( + identifier: T, uuid: string, options: { /** The unit to use for the sample. */ - unit?: string + unit?: UnitForIdentifier } = {}, ) { - const [sample, setSample] = useState() + const [sample, setSample] = useState>() const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) diff --git a/packages/react-native-healthkit/src/hooks/useStatisticsForQuantity.ts b/packages/react-native-healthkit/src/hooks/useStatisticsForQuantity.ts index 0ad26976..101580ff 100644 --- a/packages/react-native-healthkit/src/hooks/useStatisticsForQuantity.ts +++ b/packages/react-native-healthkit/src/hooks/useStatisticsForQuantity.ts @@ -3,6 +3,7 @@ import { QuantityTypes } from '../modules' import type { QueryStatisticsResponse, StatisticsOptions, + UnitForIdentifier, } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' import useSubscribeToChanges from './useSubscribeToChanges' @@ -14,7 +15,7 @@ export function useStatisticsForQuantity< options: readonly StatisticsOptions[], from: Date, to?: Date, - unit?: string, + unit?: UnitForIdentifier, ) { const [result, setResult] = useState(null) diff --git a/packages/react-native-healthkit/src/hooks/useSubscribeToQuantitySamples.ts b/packages/react-native-healthkit/src/hooks/useSubscribeToQuantitySamples.ts index c35fb4fb..591287fa 100644 --- a/packages/react-native-healthkit/src/hooks/useSubscribeToQuantitySamples.ts +++ b/packages/react-native-healthkit/src/hooks/useSubscribeToQuantitySamples.ts @@ -9,7 +9,7 @@ export function useSubscribeToQuantitySamples< TIdentifier extends QuantityTypeIdentifier, >( identifier: TIdentifier, - onChange: (args: OnQuantitySamplesCallback) => void, + onChange: (args: OnQuantitySamplesCallback) => void, ): void { const onChangeRef = useRef(onChange) diff --git a/packages/react-native-healthkit/src/modules.ts b/packages/react-native-healthkit/src/modules.ts index bee2a746..e695ffe0 100644 --- a/packages/react-native-healthkit/src/modules.ts +++ b/packages/react-native-healthkit/src/modules.ts @@ -9,7 +9,10 @@ import type { CorrelationTypeModule } from './specs/CorrelationTypeModule.nitro' import type { ElectrocardiogramModule } from './specs/ElectrocardiogramModule.nitro' import type { HeartbeatSeriesModule } from './specs/HeartbeatSeriesModule.nitro' import type { MedicationModule } from './specs/MedicationModule.nitro' -import type { QuantityTypeModule } from './specs/QuantityTypeModule.nitro' +import type { + QuantityTypeModule, + QuantityTypeModuleTyped, +} from './specs/QuantityTypeModule.nitro' import type { StateOfMindModule } from './specs/StateOfMindModule.nitro' import type { WorkoutsModule } from './specs/WorkoutsModule.nitro' @@ -27,7 +30,9 @@ export const Characteristics = ) export const QuantityTypes = - NitroModules.createHybridObject('QuantityTypeModule') + NitroModules.createHybridObject( + 'QuantityTypeModule', + ) as QuantityTypeModuleTyped export const CategoryTypes = NitroModules.createHybridObject( diff --git a/packages/react-native-healthkit/src/specs/CategoryTypeModule.nitro.ts b/packages/react-native-healthkit/src/specs/CategoryTypeModule.nitro.ts index ac06bbad..74a9d67f 100644 --- a/packages/react-native-healthkit/src/specs/CategoryTypeModule.nitro.ts +++ b/packages/react-native-healthkit/src/specs/CategoryTypeModule.nitro.ts @@ -8,7 +8,10 @@ import type { CategoryValueForIdentifier, MetadataForCategoryIdentifier, } from '../types/CategoryType' -import type { CategoryTypeIdentifier } from '../types/CategoryTypeIdentifier' +import type { + CategoryTypeIdentifier, + CategoryTypeIdentifierWriteable, +} from '../types/CategoryTypeIdentifier' import type { InterfaceAssertion } from '../types/InterfaceVerification' import type { QueryOptionsWithAnchor, @@ -17,7 +20,7 @@ import type { export interface CategoryTypeModule extends HybridObject<{ ios: 'swift' }> { saveCategorySample( - identifier: CategoryTypeIdentifier, + identifier: CategoryTypeIdentifierWriteable, value: CategoryValueForIdentifier, startDate: Date, endDate: Date, @@ -44,13 +47,13 @@ const _interfaceVerification: InterfaceAssertion< > = true export interface CategoryTypeModuleTyped { - saveCategorySample( + saveCategorySample( identifier: T, - value: CategoryValueForIdentifier, + value: CategoryValueForIdentifier, startDate: Date, endDate: Date, metadata?: MetadataForCategoryIdentifier, - ): Promise + ): Promise | undefined> queryCategorySamples( identifier: T, diff --git a/packages/react-native-healthkit/src/specs/QuantityTypeModule.nitro.ts b/packages/react-native-healthkit/src/specs/QuantityTypeModule.nitro.ts index 8b6ed792..889aa5d3 100644 --- a/packages/react-native-healthkit/src/specs/QuantityTypeModule.nitro.ts +++ b/packages/react-native-healthkit/src/specs/QuantityTypeModule.nitro.ts @@ -1,20 +1,30 @@ import type { AnyMap, HybridObject } from 'react-native-nitro-modules' -import type { QuantitySample } from '../types/QuantitySample' +import type { InterfaceAssertion } from '../types/InterfaceVerification' +import type { + MetadataForQuantityIdentifier, + QuantitySample, + QuantitySampleTyped, +} from '../types/QuantitySample' import type { AggregationStyle, IntervalComponents, QuantitySamplesWithAnchorResponse, + QuantitySamplesWithAnchorResponseTyped, QueryStatisticsResponse, QueryStatisticsResponseFromSingleSource, StatisticsOptions, StatisticsQueryOptions, + StatisticsQueryOptionsWithStringUnit, + UnitForIdentifier, } from '../types/QuantityType' import type { QuantityTypeIdentifier, QuantityTypeIdentifierWriteable, } from '../types/QuantityTypeIdentifier' import type { + QueryOptionsWithAnchorAndStringUnit, QueryOptionsWithAnchorAndUnit, + QueryOptionsWithSortOrderAndStringUnit, QueryOptionsWithSortOrderAndUnit, } from '../types/QueryOptions' @@ -37,13 +47,13 @@ export interface QuantityTypeModule extends HybridObject<{ ios: 'swift' }> { queryQuantitySamples( identifier: QuantityTypeIdentifier, - options: QueryOptionsWithSortOrderAndUnit, + options: QueryOptionsWithSortOrderAndStringUnit, ): Promise queryStatisticsForQuantity( identifier: QuantityTypeIdentifier, statistics: readonly StatisticsOptions[], - options?: StatisticsQueryOptions, + options?: StatisticsQueryOptionsWithStringUnit, ): Promise queryStatisticsCollectionForQuantity( @@ -51,13 +61,13 @@ export interface QuantityTypeModule extends HybridObject<{ ios: 'swift' }> { statistics: readonly StatisticsOptions[], anchorDate: Date, intervalComponents: IntervalComponents, - options?: StatisticsQueryOptions, + options?: StatisticsQueryOptionsWithStringUnit, ): Promise queryStatisticsForQuantitySeparateBySource( identifier: QuantityTypeIdentifier, statistics: readonly StatisticsOptions[], - options?: StatisticsQueryOptions, + options?: StatisticsQueryOptionsWithStringUnit, ): Promise queryStatisticsCollectionForQuantitySeparateBySource( @@ -65,11 +75,75 @@ export interface QuantityTypeModule extends HybridObject<{ ios: 'swift' }> { statistics: readonly StatisticsOptions[], anchorDate: Date, intervalComponents: IntervalComponents, - options?: StatisticsQueryOptions, + options?: StatisticsQueryOptionsWithStringUnit, ): Promise queryQuantitySamplesWithAnchor( identifier: QuantityTypeIdentifier, - options: QueryOptionsWithAnchorAndUnit, + options: QueryOptionsWithAnchorAndStringUnit, ): Promise } + +const _interfaceVerification: InterfaceAssertion< + QuantityTypeModule, + QuantityTypeModuleTyped, + keyof HybridObject<{ ios: 'swift' }> +> = true + +export interface QuantityTypeModuleTyped { + isQuantityCompatibleWithUnit( + identifier: T, + unit: UnitForIdentifier, + ): boolean + + aggregationStyle(identifier: QuantityTypeIdentifier): AggregationStyle + + saveQuantitySample( + identifier: T, + unit: UnitForIdentifier, + value: number, + start: Date, + end: Date, + metadata?: MetadataForQuantityIdentifier, + ): Promise | undefined> + + queryQuantitySamples( + identifier: T, + options: QueryOptionsWithSortOrderAndUnit>, + ): Promise[]> + + queryStatisticsForQuantity( + identifier: T, + statistics: readonly StatisticsOptions[], + options?: StatisticsQueryOptions>, + ): Promise + + queryStatisticsCollectionForQuantity( + identifier: T, + statistics: readonly StatisticsOptions[], + anchorDate: Date, + intervalComponents: IntervalComponents, + options?: StatisticsQueryOptions>, + ): Promise + + queryStatisticsForQuantitySeparateBySource( + identifier: T, + statistics: readonly StatisticsOptions[], + options?: StatisticsQueryOptions>, + ): Promise + + queryStatisticsCollectionForQuantitySeparateBySource< + T extends QuantityTypeIdentifier, + >( + identifier: T, + statistics: readonly StatisticsOptions[], + anchorDate: Date, + intervalComponents: IntervalComponents, + options?: StatisticsQueryOptions>, + ): Promise + + queryQuantitySamplesWithAnchor( + identifier: T, + options: QueryOptionsWithAnchorAndUnit>, + ): Promise> +} diff --git a/packages/react-native-healthkit/src/type-tests/generated-typing.ts b/packages/react-native-healthkit/src/type-tests/generated-typing.ts new file mode 100644 index 00000000..b26b4b6d --- /dev/null +++ b/packages/react-native-healthkit/src/type-tests/generated-typing.ts @@ -0,0 +1,133 @@ +import type { + BloodGlucoseUnit, + CategorySampleTyped, + CategoryTypeIdentifier, + CategoryTypeIdentifierWriteable, + CategoryValueForIdentifier, + CategoryValueSleepAnalysis, + CorrelationSampleTyped, + HeartRateMotionContext, + Quantity, + QuantitySampleTyped, + QueryOptionsWithAnchorAndUnit, + QueryOptionsWithSortOrderAndUnit, + StateOfMindSampleTyped, + StatisticsQueryOptions, + SwimmingStrokeStyle, + WorkoutEventTyped, + WorkoutSampleTyped, +} from '../types' + +type Equal = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false + +type Assert = T + +type _sleepAnalysisValuesAreTyped = Assert< + Equal< + CategoryValueForIdentifier<'HKCategoryTypeIdentifierSleepAnalysis'>, + CategoryValueSleepAnalysis + > +> + +type _newCategoryIdentifierFromSdkIsPresent = Assert< + 'HKCategoryTypeIdentifierHypertensionEvent' extends CategoryTypeIdentifier + ? true + : false +> + +type _heartRateMetadataNarrowsToEnum = Assert< + Equal< + QuantitySampleTyped<'HKQuantityTypeIdentifierHeartRate'>['metadata']['HKHeartRateMotionContext'], + HeartRateMotionContext | undefined + > +> + +type _categoryMetadataIncludesKnownSampleFields = Assert< + Equal< + CategorySampleTyped<'HKCategoryTypeIdentifierSleepAnalysis'>['metadata']['HKWasUserEntered'], + boolean | undefined + > +> + +type _heartRateEventMetadataIsTypedOnMetadata = Assert< + Equal< + QuantitySampleTyped<'HKQuantityTypeIdentifierHeartRate'>['metadata']['HKAppleDeviceCalibrated'], + boolean | undefined + > +> + +type _quantityMetadataIncludesEstimateDate = Assert< + Equal< + QuantitySampleTyped<'HKQuantityTypeIdentifierVO2Max'>['metadata']['HKDateOfEarliestDataUsedForEstimate'], + string | undefined + > +> + +type _heartRateEventThresholdMetadataIsTyped = Assert< + Equal< + CategorySampleTyped<'HKCategoryTypeIdentifierHighHeartRateEvent'>['metadata']['HKHeartRateEventThreshold'], + Quantity | undefined + > +> + +type _bloodGlucoseUnitNarrows = Assert< + Equal< + QuantitySampleTyped<'HKQuantityTypeIdentifierBloodGlucose'>['unit'], + BloodGlucoseUnit + > +> + +type _quantityQueryOptionsUnitNarrows = Assert< + Equal< + QueryOptionsWithSortOrderAndUnit['unit'], + BloodGlucoseUnit | undefined + > +> + +type _quantityAnchorQueryOptionsUnitNarrows = Assert< + Equal< + QueryOptionsWithAnchorAndUnit['unit'], + BloodGlucoseUnit | undefined + > +> + +type _quantityStatisticsOptionsUnitNarrows = Assert< + Equal< + StatisticsQueryOptions['unit'], + BloodGlucoseUnit | undefined + > +> + +type _categorySaveIdentifierIsWriteableOnly = Assert< + 'HKCategoryTypeIdentifierHighHeartRateEvent' extends CategoryTypeIdentifierWriteable + ? false + : true +> + +type _stateOfMindMetadataIsTyped = Assert< + Equal< + StateOfMindSampleTyped['metadata']['HKWasUserEntered'], + boolean | undefined + > +> + +type _correlationMetadataIsTyped = Assert< + Equal +> + +type _workoutMetadataIsTypedOnMetadata = Assert< + Equal< + WorkoutSampleTyped['metadata']['HKWorkoutBrandName'], + string | undefined + > +> + +type _workoutEventMetadataIsTypedOnMetadata = Assert< + Equal< + NonNullable['HKSwimmingStrokeStyle'], + SwimmingStrokeStyle | undefined + > +> diff --git a/packages/react-native-healthkit/src/types/CategoryType.ts b/packages/react-native-healthkit/src/types/CategoryType.ts index 613192b3..7fb426a0 100644 --- a/packages/react-native-healthkit/src/types/CategoryType.ts +++ b/packages/react-native-healthkit/src/types/CategoryType.ts @@ -1,26 +1,62 @@ import type { AnyMap } from 'react-native-nitro-modules' +import type { + CategoryTypedMetadataForIdentifierGenerated, + CategoryValueForIdentifierGenerated, +} from '../generated/healthkit.generated' +import { + CategoryValueAppetiteChanges, + CategoryValueAppleStandHour, + CategoryValueAppleWalkingSteadinessEvent, + CategoryValueCervicalMucusQuality, + CategoryValueContraceptive, + CategoryValueEnvironmentalAudioExposureEvent, + CategoryValueHeadphoneAudioExposureEvent, + CategoryValueLowCardioFitnessEvent, + CategoryValueMenstrualFlow, + CategoryValue as CategoryValueNotApplicable, + CategoryValueOvulationTestResult, + CategoryValuePregnancyTestResult, + CategoryValuePresence, + CategoryValueProgesteroneTestResult, + CategoryValueSeverity, + CategoryValueSleepAnalysis, + CategoryValueVaginalBleeding, +} from '../generated/healthkit.generated' import type { CategoryTypeIdentifier } from './CategoryTypeIdentifier' import type { BaseSample, DeletedSample, GenericMetadata } from './Shared' import type { SourceRevision } from './Source' +export { + CategoryValueAppetiteChanges, + CategoryValueAppleStandHour, + CategoryValueAppleWalkingSteadinessEvent, + CategoryValueCervicalMucusQuality, + CategoryValueContraceptive, + CategoryValueEnvironmentalAudioExposureEvent, + CategoryValueHeadphoneAudioExposureEvent, + CategoryValueLowCardioFitnessEvent, + CategoryValueMenstrualFlow, + CategoryValueNotApplicable, + CategoryValueOvulationTestResult, + CategoryValuePregnancyTestResult, + CategoryValuePresence, + CategoryValueProgesteroneTestResult, + CategoryValueSeverity, + CategoryValueSleepAnalysis, + CategoryValueVaginalBleeding, +} + export type CategoryTypePresenceIdentifier = | 'HKCategoryTypeIdentifierAppetiteChanges' | 'HKCategoryTypeIdentifierSleepChanges' export type CategoryTypeValueNotApplicableIdentifier = | 'HKCategoryTypeIdentifierHighHeartRateEvent' + | 'HKCategoryTypeIdentifierHypertensionEvent' | 'HKCategoryTypeIdentifierIntermenstrualBleeding' | 'HKCategoryTypeIdentifierMindfulSession' | 'HKCategoryTypeIdentifierSexualActivity' - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluepregnancytestresult Apple Docs } - */ -enum CategoryValuePregnancyTestResult { - positive = 2, - negative = 1, - indeterminate = 3, -} + | 'HKCategoryTypeIdentifierSleepApneaEvent' export type CategoryTypeSeverityIdentifier = | 'HKCategoryTypeIdentifierAbdominalCramps' @@ -47,7 +83,6 @@ export type CategoryTypeSeverityIdentifier = | 'HKCategoryTypeIdentifierLossOfTaste' | 'HKCategoryTypeIdentifierLowerBackPain' | 'HKCategoryTypeIdentifierMemoryLapse' - | 'HKCategoryTypeIdentifierMoodChanges' | 'HKCategoryTypeIdentifierNausea' | 'HKCategoryTypeIdentifierNightSweats' | 'HKCategoryTypeIdentifierPelvicPain' @@ -61,109 +96,10 @@ export type CategoryTypeSeverityIdentifier = | 'HKCategoryTypeIdentifierVomiting' | 'HKCategoryTypeIdentifierWheezing' -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluecervicalmucusquality Apple Docs } - */ -export enum CategoryValueCervicalMucusQuality { - dry = 1, - sticky = 2, - creamy = 3, - watery = 4, - eggWhite = 5, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluemenstrualflow Apple Docs } - */ -export enum CategoryValueMenstrualFlow { - unspecified = 1, - none = 5, - light = 2, - medium = 3, - heavy = 4, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvalueovulationtestresult Apple Docs } - */ -export enum CategoryValueOvulationTestResult { - negative = 1, - luteinizingHormoneSurge = 2, - indeterminate = 3, - estrogenSurge = 4, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis Apple Docs } - */ -export enum CategoryValueSleepAnalysis { - inBed = 0, - asleepUnspecified = 1, - awake = 2, - asleepCore = 3, - asleepDeep = 4, - asleepREM = 5, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvalueappetitechanges Apple Docs} - */ -export enum CategoryValueAppetiteChanges { - decreased = 2, - increased = 3, - noChange = 1, - unspecified = 0, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluepresence Apple Docs} - */ -export enum CategoryValuePresence { - notPresent = 1, - present = 0, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvalueseverity Apple Docs } - */ -export enum CategoryValueSeverity { - notPresent = 1, - mild = 2, - moderate = 3, - severe = 4, - unspecified = 0, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvalue/notapplicable Apple Docs } - */ -export enum CategoryValueNotApplicable { - notApplicable = 0, -} - -export enum CategoryValueLowCardioFitnessEvent { - lowFitness = 1, -} - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvalue Apple Docs } - */ export type CategoryValue = - | CategoryValueAppetiteChanges - | CategoryValueCervicalMucusQuality - | CategoryValueLowCardioFitnessEvent - | CategoryValueMenstrualFlow - | CategoryValueOvulationTestResult - | CategoryValuePresence - | CategoryValueSeverity - | CategoryValueSleepAnalysis + | CategoryValueForIdentifierGenerated | number -export enum CategoryValueAppleStandHour { - stood = 0, - idle = 1, -} - export interface CategorySampleForSaving { readonly start: Date readonly end: Date @@ -194,49 +130,17 @@ export interface CategorySamplesWithAnchorResponseTyped< export type MetadataForCategoryIdentifier< T extends CategoryTypeIdentifier = CategoryTypeIdentifier, -> = T extends 'HKCategoryTypeIdentifierSexualActivity' - ? GenericMetadata & { - readonly HKSexualActivityProtectionUsed?: boolean - } - : T extends 'HKCategoryTypeIdentifierMenstrualFlow' - ? GenericMetadata & { - readonly HKMenstrualCycleStart?: boolean - } - : GenericMetadata +> = GenericMetadata & CategoryTypedMetadataForIdentifierGenerated -export interface CategorySampleTyped - extends Omit { +export type CategorySampleTyped = Omit< + CategorySample, + 'categoryType' | 'value' | 'metadata' +> & { readonly categoryType: T readonly value: CategoryValueForIdentifier - readonly metadata: MetadataForCategoryIdentifier - - readonly metadataSexualActivityProtectionUsed?: boolean - readonly metadataMenstrualCycleStart?: boolean } export type CategoryValueForIdentifier< T extends CategoryTypeIdentifier = CategoryTypeIdentifier, -> = T extends 'HKCategoryTypeIdentifierCervicalMucusQuality' - ? CategoryValueCervicalMucusQuality - : T extends 'CategoryTypeIdentifierMenstrualFlow' - ? CategoryValueMenstrualFlow - : T extends 'HKCategoryTypeIdentifierOvulationTestResult' - ? CategoryValueOvulationTestResult - : T extends 'HKCategoryTypeIdentifierSleepAnalysis' - ? CategoryValueSleepAnalysis - : T extends CategoryTypeValueNotApplicableIdentifier - ? CategoryValueNotApplicable - : T extends CategoryTypeSeverityIdentifier - ? CategoryValueSeverity - : T extends CategoryTypePresenceIdentifier - ? CategoryValuePresence - : T extends 'HKCategoryTypeIdentifierLowCardioFitnessEvent' - ? CategoryValueLowCardioFitnessEvent - : T extends 'HKCategoryTypeIdentifierPregnancyTestResult' - ? CategoryValuePregnancyTestResult - : T extends 'HKCategoryTypeIdentifierPregnancyTestResult' - ? CategoryValuePregnancyTestResult - : T extends 'HKCategoryTypeIdentifierAppleStandHour' - ? CategoryValueAppleStandHour - : number +> = CategoryValueForIdentifierGenerated diff --git a/packages/react-native-healthkit/src/types/CategoryTypeIdentifier.ts b/packages/react-native-healthkit/src/types/CategoryTypeIdentifier.ts index 5d9e8353..c825ecef 100644 --- a/packages/react-native-healthkit/src/types/CategoryTypeIdentifier.ts +++ b/packages/react-native-healthkit/src/types/CategoryTypeIdentifier.ts @@ -1,134 +1,5 @@ -export type CategoryTypeIdentifierReadOnly = - | 'HKCategoryTypeIdentifierAppleStandHour' - | 'HKCategoryTypeIdentifierHighHeartRateEvent' - | 'HKCategoryTypeIdentifierLowHeartRateEvent' - | 'HKCategoryTypeIdentifierHeadphoneAudioExposureEvent' - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier Apple Docs } - */ -export type CategoryTypeIdentifierWriteable = - | 'HKCategoryTypeIdentifierSleepAnalysis' - | 'HKCategoryTypeIdentifierCervicalMucusQuality' - | 'HKCategoryTypeIdentifierOvulationTestResult' - /** - * @deprecated In iOS 18 beta - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifiermenstrualflow Apple Docs } - */ - | 'HKCategoryTypeIdentifierMenstrualFlow' - | 'HKCategoryTypeIdentifierIntermenstrualBleeding' - | 'HKCategoryTypeIdentifierSexualActivity' - | 'HKCategoryTypeIdentifierMindfulSession' - | 'HKCategoryTypeIdentifierIrregularHeartRhythmEvent' - /** - * @deprecated Use environmentalAudioExposureEvent instead. - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/audioexposureevent Apple Docs } - */ - | 'HKCategoryTypeIdentifierAudioExposureEvent' - | 'HKCategoryTypeIdentifierToothbrushingEvent' - | 'HKCategoryTypeIdentifierLowCardioFitnessEvent' - | 'HKCategoryTypeIdentifierContraceptive' - | 'HKCategoryTypeIdentifierLactation' - | 'HKCategoryTypeIdentifierPregnancy' - | 'HKCategoryTypeIdentifierPregnancyTestResult' - | 'HKCategoryTypeIdentifierProgesteroneTestResult' - | 'HKCategoryTypeIdentifierEnvironmentalAudioExposureEvent' - | 'HKCategoryTypeIdentifierAppleWalkingSteadinessEvent' - | 'HKCategoryTypeIdentifierHandwashingEvent' - - // Symptoms - | 'HKCategoryTypeIdentifierAbdominalCramps' - | 'HKCategoryTypeIdentifierAcne' - | 'HKCategoryTypeIdentifierAppetiteChanges' - | 'HKCategoryTypeIdentifierBladderIncontinence' - | 'HKCategoryTypeIdentifierBloating' - | 'HKCategoryTypeIdentifierBreastPain' - | 'HKCategoryTypeIdentifierChestTightnessOrPain' - | 'HKCategoryTypeIdentifierChills' - | 'HKCategoryTypeIdentifierConstipation' - | 'HKCategoryTypeIdentifierCoughing' - | 'HKCategoryTypeIdentifierDiarrhea' - | 'HKCategoryTypeIdentifierDizziness' - | 'HKCategoryTypeIdentifierDrySkin' - | 'HKCategoryTypeIdentifierFainting' - | 'HKCategoryTypeIdentifierFatigue' - | 'HKCategoryTypeIdentifierFever' - | 'HKCategoryTypeIdentifierGeneralizedBodyAche' - | 'HKCategoryTypeIdentifierHairLoss' - | 'HKCategoryTypeIdentifierHeadache' - | 'HKCategoryTypeIdentifierHeartburn' - | 'HKCategoryTypeIdentifierHotFlashes' - | 'HKCategoryTypeIdentifierLossOfSmell' - | 'HKCategoryTypeIdentifierLossOfTaste' - | 'HKCategoryTypeIdentifierLowerBackPain' - | 'HKCategoryTypeIdentifierMemoryLapse' - | 'HKCategoryTypeIdentifierMoodChanges' - | 'HKCategoryTypeIdentifierNausea' - | 'HKCategoryTypeIdentifierNightSweats' - | 'HKCategoryTypeIdentifierPelvicPain' - | 'HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat' - | 'HKCategoryTypeIdentifierRunnyNose' - | 'HKCategoryTypeIdentifierShortnessOfBreath' - | 'HKCategoryTypeIdentifierSinusCongestion' - | 'HKCategoryTypeIdentifierSkippedHeartbeat' - | 'HKCategoryTypeIdentifierSleepChanges' - | 'HKCategoryTypeIdentifierSoreThroat' - | 'HKCategoryTypeIdentifierVaginalDryness' - | 'HKCategoryTypeIdentifierVomiting' - | 'HKCategoryTypeIdentifierWheezing' - - /** - * Bleeding After Pregnancy - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifierbleedingafterpregnancy Apple Docs } - * @since iOS 18 - */ - | 'HKCategoryTypeIdentifierBleedingAfterPregnancy' - - /** - * Bleeding During Pregnancy - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifierbleedingduringpregnancy Apple Docs } - * @since iOS 18 - */ - | 'HKCategoryTypeIdentifierBleedingDuringPregnancy' - - /** - * Infrequent Menstrual Cycles - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/infrequentmenstrualcycles Apple Docs } - * @since iOS 16 - */ - | 'HKCategoryTypeIdentifierInfrequentMenstrualCycles' - - /** - * Irregular Menstrual Cycles - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/irregularmenstrualcycles Apple Docs } - * @since iOS 16 - */ - | 'HKCategoryTypeIdentifierIrregularMenstrualCycles' - - /** - * Persistent Intermenstrual Bleeding - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/persistentintermenstrualbleeding Apple Docs } - * @since iOS 16 - */ - | 'HKCategoryTypeIdentifierPersistentIntermenstrualBleeding' - - /** - * Prolonged Menstrual Periods - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/prolongedmenstrualperiods Apple Docs } - * @since iOS 16 - */ - | 'HKCategoryTypeIdentifierProlongedMenstrualPeriods' - - /** - * Sleep Apnea Event - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/sleepapneaevent Apple Docs } - * @since iOS 18 - */ - | 'HKCategoryTypeIdentifierSleepApneaEvent' - -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier Apple Docs } - */ -export type CategoryTypeIdentifier = - | CategoryTypeIdentifierReadOnly - | CategoryTypeIdentifierWriteable +export type { + CategoryTypeIdentifier, + CategoryTypeIdentifierReadOnly, + CategoryTypeIdentifierWriteable, +} from '../generated/healthkit.generated' diff --git a/packages/react-native-healthkit/src/types/CorrelationType.ts b/packages/react-native-healthkit/src/types/CorrelationType.ts index 188b3693..83207135 100644 --- a/packages/react-native-healthkit/src/types/CorrelationType.ts +++ b/packages/react-native-healthkit/src/types/CorrelationType.ts @@ -1,6 +1,7 @@ +import type { KnownSampleMetadata } from '../generated/healthkit.generated' import type { CategorySample, CategorySampleForSaving } from './CategoryType' import type { QuantitySample, QuantitySampleForSaving } from './QuantitySample' -import type { BaseSample, DeletedSample } from './Shared' +import type { BaseSample, DeletedSample, WithTypedMetadata } from './Shared' /** * @see {@link https://developer.apple.com/documentation/healthkit/hkcorrelationtypeidentifier Apple Docs } @@ -14,10 +15,13 @@ type CorrelationObject = CategorySample | QuantitySample export interface CorrelationSample extends BaseSample { readonly correlationType: CorrelationTypeIdentifier readonly objects: readonly CorrelationObject[] - - readonly metadataFoodType?: string } +export type CorrelationSampleTyped = WithTypedMetadata< + CorrelationSample, + KnownSampleMetadata +> + export type SampleForSaving = CategorySampleForSaving | QuantitySampleForSaving export interface QueryCorrelationSamplesWithAnchorResponse { @@ -25,3 +29,9 @@ export interface QueryCorrelationSamplesWithAnchorResponse { readonly deletedSamples: readonly DeletedSample[] readonly newAnchor: string } + +export interface QueryCorrelationSamplesWithAnchorResponseTyped { + readonly correlations: readonly CorrelationSampleTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} diff --git a/packages/react-native-healthkit/src/types/ElectrocardiogramSample.ts b/packages/react-native-healthkit/src/types/ElectrocardiogramSample.ts index 84e9a319..894800cf 100644 --- a/packages/react-native-healthkit/src/types/ElectrocardiogramSample.ts +++ b/packages/react-native-healthkit/src/types/ElectrocardiogramSample.ts @@ -1,8 +1,9 @@ +import type { KnownSampleMetadata } from '../generated/healthkit.generated' import type { QueryOptionsWithAnchor, QueryOptionsWithSortOrder, } from '../types/QueryOptions' -import type { BaseSample, DeletedSample } from './Shared' +import type { BaseSample, DeletedSample, WithTypedMetadata } from './Shared' // Enums mirror HealthKit; keep the union literal names stable. export type ElectrocardiogramClassification = @@ -33,12 +34,23 @@ export interface ElectrocardiogramSample extends BaseSample { readonly voltages?: readonly ElectrocardiogramVoltage[] } +export type ElectrocardiogramSampleTyped = WithTypedMetadata< + ElectrocardiogramSample, + KnownSampleMetadata +> + export interface ElectrocardiogramSamplesWithAnchorResponse { readonly samples: readonly ElectrocardiogramSample[] readonly deletedSamples: readonly DeletedSample[] readonly newAnchor: string } +export interface ElectrocardiogramSamplesWithAnchorResponseTyped { + readonly samples: readonly ElectrocardiogramSampleTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} + export interface ECGQueryOptionsWithSortOrder extends QueryOptionsWithSortOrder { readonly includeVoltages?: boolean // default false diff --git a/packages/react-native-healthkit/src/types/HeartbeatSeries.ts b/packages/react-native-healthkit/src/types/HeartbeatSeries.ts index a6abca3f..01903a8e 100644 --- a/packages/react-native-healthkit/src/types/HeartbeatSeries.ts +++ b/packages/react-native-healthkit/src/types/HeartbeatSeries.ts @@ -1,4 +1,5 @@ -import type { BaseSample, DeletedSample } from './Shared' +import type { KnownSampleMetadata } from '../generated/healthkit.generated' +import type { BaseSample, DeletedSample, WithTypedMetadata } from './Shared' export interface Heartbeat { readonly timeSinceSeriesStart: number @@ -9,8 +10,19 @@ export interface HeartbeatSeriesSample extends BaseSample { readonly heartbeats: readonly Heartbeat[] } +export type HeartbeatSeriesSampleTyped = WithTypedMetadata< + HeartbeatSeriesSample, + KnownSampleMetadata +> + export interface HeartbeatSeriesSamplesWithAnchorResponse { readonly samples: readonly HeartbeatSeriesSample[] readonly deletedSamples: readonly DeletedSample[] readonly newAnchor: string } + +export interface HeartbeatSeriesSamplesWithAnchorResponseTyped { + readonly samples: readonly HeartbeatSeriesSampleTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} diff --git a/packages/react-native-healthkit/src/types/InterfaceVerification.ts b/packages/react-native-healthkit/src/types/InterfaceVerification.ts index a71b3bb8..c925b2ec 100644 --- a/packages/react-native-healthkit/src/types/InterfaceVerification.ts +++ b/packages/react-native-healthkit/src/types/InterfaceVerification.ts @@ -90,6 +90,25 @@ export type CheckParameterCounts< ExtractMethodNames] } +/** + * Checks that typed methods remain assignable to their base counterparts. + */ +export type CheckMethodCompatibility< + BaseInterface, + TypedInterface, + ExcludeFromBase extends keyof BaseInterface = never, +> = { + SharedMethodNames: ExtractMethodNames & + ExtractMethodNames + IncompatibleMethods: { + [K in ExtractMethodNames & + ExtractMethodNames]: TypedInterface[K] extends BaseInterface[K] + ? never + : K + }[ExtractMethodNames & + ExtractMethodNames] +} + /** * Complete interface verification that checks both method names and parameter counts */ @@ -104,6 +123,11 @@ export type VerifyInterfaceSync< TypedInterface, ExcludeFromBase > + CompatibilityCheck: CheckMethodCompatibility< + BaseInterface, + TypedInterface, + ExcludeFromBase + > // Final verification result Result: CheckMethodNames< @@ -116,7 +140,20 @@ export type VerifyInterfaceSync< TypedInterface, ExcludeFromBase >['ParameterCountMismatches'] extends never - ? true + ? CheckMethodCompatibility< + BaseInterface, + TypedInterface, + ExcludeFromBase + >['IncompatibleMethods'] extends never + ? true + : { + ERROR: 'Method signature compatibility mismatch detected' + IncompatibleMethods: CheckMethodCompatibility< + BaseInterface, + TypedInterface, + ExcludeFromBase + >['IncompatibleMethods'] + } : { ERROR: 'Parameter count mismatch detected' MethodsWithParameterCountMismatch: CheckParameterCounts< diff --git a/packages/react-native-healthkit/src/types/Medication.ts b/packages/react-native-healthkit/src/types/Medication.ts new file mode 100644 index 00000000..7deedc51 --- /dev/null +++ b/packages/react-native-healthkit/src/types/Medication.ts @@ -0,0 +1,19 @@ +import type { KnownSampleMetadata } from '../generated/healthkit.generated' +import type { + MedicationDoseEvent, + UserAnnotatedMedication, +} from '../specs/MedicationModule.nitro' +import type { DeletedSample, WithTypedMetadata } from './Shared' + +export type { UserAnnotatedMedication } + +export type MedicationDoseEventTyped = WithTypedMetadata< + MedicationDoseEvent, + KnownSampleMetadata +> + +export interface MedicationDoseEventsWithAnchorResponseTyped { + readonly samples: readonly MedicationDoseEventTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} diff --git a/packages/react-native-healthkit/src/types/MetadataEnums.ts b/packages/react-native-healthkit/src/types/MetadataEnums.ts new file mode 100644 index 00000000..5f377670 --- /dev/null +++ b/packages/react-native-healthkit/src/types/MetadataEnums.ts @@ -0,0 +1,15 @@ +export { + AppleECGAlgorithmVersion, + BloodGlucoseMealTime, + BodyTemperatureSensorLocation, + CyclingFunctionalThresholdPowerTestType, + DevicePlacementSide, + HeartRateRecoveryTestType, + HeartRateSensorLocation, + PhysicalEffortEstimationType, + SwimmingStrokeStyle, + UserMotionContext, + VO2MaxTestType, + WaterSalinity, + WorkoutSwimmingLocationType, +} from '../generated/healthkit.generated' diff --git a/packages/react-native-healthkit/src/types/QuantitySample.ts b/packages/react-native-healthkit/src/types/QuantitySample.ts index 5bcc5d07..964a4cbe 100644 --- a/packages/react-native-healthkit/src/types/QuantitySample.ts +++ b/packages/react-native-healthkit/src/types/QuantitySample.ts @@ -1,7 +1,8 @@ import type { AnyMap } from 'react-native-nitro-modules' - +import type { QuantityTypedMetadataForIdentifierGenerated } from '../generated/healthkit.generated' +import type { UnitForIdentifier } from './QuantityType' import type { QuantityTypeIdentifier } from './QuantityTypeIdentifier' -import type { BaseSample } from './Shared' +import type { BaseSample, MetadataWithUnknown } from './Shared' import type { SourceRevision } from './Source' /** @@ -13,6 +14,17 @@ export interface QuantitySample extends BaseSample { readonly unit: string } +export type MetadataForQuantityIdentifier< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> = MetadataWithUnknown> + +export interface QuantitySampleTyped + extends Omit { + readonly quantityType: T + readonly unit: UnitForIdentifier + readonly metadata: MetadataForQuantityIdentifier +} + export interface QuantitySampleForSaving { readonly startDate: Date readonly endDate: Date diff --git a/packages/react-native-healthkit/src/types/QuantityType.ts b/packages/react-native-healthkit/src/types/QuantityType.ts index b3c2ef3e..73f6a244 100644 --- a/packages/react-native-healthkit/src/types/QuantityType.ts +++ b/packages/react-native-healthkit/src/types/QuantityType.ts @@ -1,20 +1,15 @@ +import type { UnitForIdentifierGenerated } from '../generated/healthkit.generated' +import { + HeartRateMotionContext, + InsulinDeliveryReason, +} from '../generated/healthkit.generated' import type { SourceProxy } from '../specs/SourceProxy.nitro' -import type { QuantitySample } from './QuantitySample' +import type { QuantitySample, QuantitySampleTyped } from './QuantitySample' import type { QuantityTypeIdentifier } from './QuantityTypeIdentifier' import type { FilterForSamples } from './QueryOptions' import type { DeletedSample } from './Shared' -import type { - BloodGlucoseUnit, - CountPerTime, - EnergyUnit, - LengthUnit, - MassUnit, - SpeedUnit, - TemperatureUnit, - TimeUnit, - Unit, - VolumeUnit, -} from './Units' + +export { HeartRateMotionContext, InsulinDeliveryReason } interface QuantityDateInterval { readonly from: Date @@ -52,25 +47,19 @@ export interface QuantitySamplesWithAnchorResponse { readonly newAnchor: string } +export interface QuantitySamplesWithAnchorResponseTyped< + T extends QuantityTypeIdentifier, +> { + readonly samples: readonly QuantitySampleTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} + export interface Quantity { readonly unit: string readonly quantity: number } -/** - * @see {@link https://developer.apple.com/documentation/healthkit/hkinsulindeliveryreason Apple Docs } - */ -export enum InsulinDeliveryReason { - basal = 1, - bolus = 2, -} - -export enum HeartRateMotionContext { - active = 2, - notSet = 0, - sedentary = 1, -} - export interface IntervalComponents { readonly minute?: number readonly hour?: number @@ -79,11 +68,14 @@ export interface IntervalComponents { readonly year?: number } -export interface StatisticsQueryOptions { +export interface StatisticsQueryOptions { filter?: FilterForSamples - unit?: string + unit?: TUnit } +export interface StatisticsQueryOptionsWithStringUnit + extends StatisticsQueryOptions {} + /** * @see {@link https://developer.apple.com/documentation/healthkit/hkstatisticsoptions Apple Docs } */ @@ -94,99 +86,7 @@ export type StatisticsOptions = | 'discreteMin' | 'duration' | 'mostRecent' -// | 'separateBySource' (removed since it's handled by separate functions) export type UnitForIdentifier< T extends QuantityTypeIdentifier = QuantityTypeIdentifier, -> = T extends 'QuantityTypeIdentifierBloodGlucose' - ? BloodGlucoseUnit - : T extends - | 'QuantityTypeIdentifierAppleExerciseTime' - | 'QuantityTypeIdentifierAppleMoveTime' - | 'QuantityTypeIdentifierAppleStandTime' - ? TimeUnit - : T extends - | 'QuantityTypeIdentifierActiveEnergyBurned' - | 'QuantityTypeIdentifierBasalEnergyBurned' - | 'QuantityTypeIdentifierDietaryEnergyConsumed' - ? EnergyUnit - : T extends - | 'QuantityTypeIdentifierDistanceCycling' - | 'QuantityTypeIdentifierDistanceDownhillSnowSports' - | 'QuantityTypeIdentifierDistanceSwimming' - | 'QuantityTypeIdentifierDistanceWalkingRunning' - | 'QuantityTypeIdentifierDistanceWheelchair' - | 'QuantityTypeIdentifierSixMinuteWalkTestDistance' - | 'QuantityTypeIdentifierWaistCircumference' - ? LengthUnit - : T extends - | 'QuantityTypeIdentifierBodyFatPercentage' - | 'QuantityTypeIdentifierOxygenSaturation' - | 'QuantityTypeIdentifierWalkingAsymmetryPercentage' - | 'QuantityTypeIdentifierWalkingDoubleSupportPercentage' - ? '%' - : T extends 'QuantityTypeIdentifierBasalBodyTemperature' - ? TemperatureUnit - : T extends - | 'QuantityTypeIdentifierRunningSpeed' - | 'QuantityTypeIdentifierStairAscentSpeed' - | 'QuantityTypeIdentifierStairDescentSpeed' - | 'QuantityTypeIdentifierWalkingSpeed' - ? SpeedUnit - : T extends - | 'QuantityTypeIdentifierFlightsClimbed' - | 'QuantityTypeIdentifierNumberOfAlcoholicBeverages' - | 'QuantityTypeIdentifierNumberOfTimesFallen' - | 'QuantityTypeIdentifierPushCount' - | 'QuantityTypeIdentifierStepCount' - | 'QuantityTypeIdentifierSwimmingStrokeCount' - ? 'count' - : T extends - | 'QuantityTypeIdentifierDietaryBiotin' - | 'QuantityTypeIdentifierDietaryCaffeine' - | 'QuantityTypeIdentifierDietaryCalcium' - | 'QuantityTypeIdentifierDietaryCarbohydrates' - | 'QuantityTypeIdentifierDietaryChloride' - | 'QuantityTypeIdentifierDietaryCholesterol' - | 'QuantityTypeIdentifierDietaryChromium' - | 'QuantityTypeIdentifierDietaryCopper' - | 'QuantityTypeIdentifierDietaryFatMonounsaturated' - | 'QuantityTypeIdentifierDietaryFatPolyunsaturated' - | 'QuantityTypeIdentifierDietaryFatSaturated' - | 'QuantityTypeIdentifierDietaryFatTotal' - | 'QuantityTypeIdentifierDietaryFiber' - | 'QuantityTypeIdentifierDietaryFolate' - | 'QuantityTypeIdentifierDietaryIodine' - | 'QuantityTypeIdentifierDietaryIron' - | 'QuantityTypeIdentifierDietaryMagnesium' - | 'QuantityTypeIdentifierDietaryManganese' - | 'QuantityTypeIdentifierDietaryMolybdenum' - | 'QuantityTypeIdentifierDietaryNiacin' - | 'QuantityTypeIdentifierDietaryPantothenicAcid' - | 'QuantityTypeIdentifierDietaryPhosphorus' - | 'QuantityTypeIdentifierDietaryPotassium' - | 'QuantityTypeIdentifierDietaryProtein' - | 'QuantityTypeIdentifierDietaryRiboflavin' - | 'QuantityTypeIdentifierDietarySelenium' - | 'QuantityTypeIdentifierDietarySodium' - | 'QuantityTypeIdentifierDietarySugar' - | 'QuantityTypeIdentifierDietaryThiamin' - | 'QuantityTypeIdentifierDietaryVitaminA' - | 'QuantityTypeIdentifierDietaryVitaminB6' - | 'QuantityTypeIdentifierDietaryVitaminB12' - | 'QuantityTypeIdentifierDietaryVitaminC' - | 'QuantityTypeIdentifierDietaryVitaminD' - | 'QuantityTypeIdentifierDietaryVitaminE' - | 'QuantityTypeIdentifierDietaryVitaminK' - | 'QuantityTypeIdentifierDietaryZinc' - ? MassUnit - : T extends 'QuantityTypeIdentifierDietaryWater' - ? VolumeUnit - : T extends 'QuantityTypeIdentifierInsulinDelivery' - ? 'IU' - : T extends - | 'QuantityTypeIdentifierHeartRate' - | 'QuantityTypeIdentifierRestingHeartRate' - | 'QuantityTypeIdentifierWalkingHeartRateAverage' - ? CountPerTime - : Unit +> = UnitForIdentifierGenerated diff --git a/packages/react-native-healthkit/src/types/QuantityTypeIdentifier.ts b/packages/react-native-healthkit/src/types/QuantityTypeIdentifier.ts index 9abfdddd..4ffcf916 100644 --- a/packages/react-native-healthkit/src/types/QuantityTypeIdentifier.ts +++ b/packages/react-native-healthkit/src/types/QuantityTypeIdentifier.ts @@ -1,772 +1,5 @@ -/** - * Represents a quantity type identifier. - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier Apple Docs HKQuantityTypeIdentifier} - */ -export type QuantityTypeIdentifierReadOnly = - /** - * Walking Heart Rate Average - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwalkingheartrateaverage Apple Docs HKQuantityTypeIdentifierWalkingHeartRateAverage} - * @since iOS 11.0 - */ - | 'HKQuantityTypeIdentifierWalkingHeartRateAverage' - - /** - * Atrial Fibrillation Burden - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifieratrialfibrillationburden Apple Docs HKQuantityTypeIdentifierAtrialFibrillationBurden} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierAtrialFibrillationBurden' // Scalar(Percent, 0.0 - 1.0), Discrete - /** - * Apple Exercise Time - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierappleexercisetime Apple Docs HKQuantityTypeIdentifierAppleExerciseTime} - */ - | 'HKQuantityTypeIdentifierAppleExerciseTime' - /** - * Apple Stand Time - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierapplestandtime Apple Docs HKQuantityTypeIdentifierAppleStandTime} - */ - | 'HKQuantityTypeIdentifierAppleStandTime' - - /** - * Apple Walking Steadiness - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierapplewalkingsteadiness Apple Docs HKQuantityTypeIdentifierAppleWalkingSteadiness} - * @since iOS 15 - */ - | 'HKQuantityTypeIdentifierAppleWalkingSteadiness' // Scalar(Percent, 0.0 - 1.0), Discrete - -/** - * Represents a quantity type identifier. - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier Apple Docs HKQuantityTypeIdentifier} - */ -export type QuantityTypeIdentifierWriteable = - /** - * Body Mass Index - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbodymassindex Apple Docs HKQuantityTypeIdentifierBodyMassIndex} - */ - | 'HKQuantityTypeIdentifierBodyMassIndex' - - /** - * Body Fat Percentage - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbodyfatpercentage Apple Docs HKQuantityTypeIdentifierBodyFatPercentage} - */ - | 'HKQuantityTypeIdentifierBodyFatPercentage' - - /** - * Height - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierheight Apple Docs HKQuantityTypeIdentifierHeight} - */ - | 'HKQuantityTypeIdentifierHeight' - - /** - * Body Mass - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbodymass Apple Docs HKQuantityTypeIdentifierBodyMass} - */ - | 'HKQuantityTypeIdentifierBodyMass' - - /** - * Lean Body Mass - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierleanbodymass Apple Docs HKQuantityTypeIdentifierLeanBodyMass} - */ - | 'HKQuantityTypeIdentifierLeanBodyMass' - - /** - * Waist Circumference - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwaistcircumference Apple Docs HKQuantityTypeIdentifierWaistCircumference} - */ - | 'HKQuantityTypeIdentifierWaistCircumference' - - /** - * Step Count - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierstepcount Apple Docs HKQuantityTypeIdentifierStepCount} - */ - | 'HKQuantityTypeIdentifierStepCount' - - /** - * Distance Walking Running - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdistancewalkingrunning Apple Docs HKQuantityTypeIdentifierDistanceWalkingRunning} - */ - | 'HKQuantityTypeIdentifierDistanceWalkingRunning' - - /** - * Distance Cycling - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdistancecycling Apple Docs HKQuantityTypeIdentifierDistanceCycling} - */ - | 'HKQuantityTypeIdentifierDistanceCycling' - - /** - * Distance Wheelchair - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdistancewheelchair Apple Docs HKQuantityTypeIdentifierDistanceWheelchair} - */ - | 'HKQuantityTypeIdentifierDistanceWheelchair' - /** - * Basal Energy Burned - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbasalenergyburned Apple Docs HKQuantityTypeIdentifierBasalEnergyBurned} - */ - | 'HKQuantityTypeIdentifierBasalEnergyBurned' - /** - * Active Energy Burned - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifieractiveenergyburned Apple Docs HKQuantityTypeIdentifierActiveEnergyBurned} - */ - | 'HKQuantityTypeIdentifierActiveEnergyBurned' - /** - * Flights Climbed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierflightsclimbed Apple Docs HKQuantityTypeIdentifierFlightsClimbed} - */ - | 'HKQuantityTypeIdentifierFlightsClimbed' - /** - * Nike Fuel - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiernikefuel Apple Docs HKQuantityTypeIdentifierNikeFuel} - */ - | 'HKQuantityTypeIdentifierNikeFuel' - /** - * Push Count - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierpushcount Apple Docs HKQuantityTypeIdentifierPushCount} - */ - | 'HKQuantityTypeIdentifierPushCount' - /** - * Distance Swimming - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdistanceswimming Apple Docs HKQuantityTypeIdentifierDistanceSwimming} - */ - | 'HKQuantityTypeIdentifierDistanceSwimming' - /** - * Swimming Stroke Count - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierswimmingstrokecount Apple Docs HKQuantityTypeIdentifierSwimmingStrokeCount} - */ - | 'HKQuantityTypeIdentifierSwimmingStrokeCount' - /** - * VO2 Max - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiervo2max Apple Docs HKQuantityTypeIdentifierVO2Max} - */ - | 'HKQuantityTypeIdentifierVO2Max' - /** - * Distance Downhill Snow Sports - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdistancedownhillsnowsports Apple Docs HKQuantityTypeIdentifierDistanceDownhillSnowSports} - */ - | 'HKQuantityTypeIdentifierDistanceDownhillSnowSports' - - // Vitals - /** - * Heart Rate - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierheartrate Apple Docs HKQuantityTypeIdentifierHeartRate} - */ - | 'HKQuantityTypeIdentifierHeartRate' - /** - * Body Temperature - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbodytemperature Apple Docs HKQuantityTypeIdentifierBodyTemperature} - */ - | 'HKQuantityTypeIdentifierBodyTemperature' - /** - * Basal Body Temperature - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbasalbodytemperature Apple Docs HKQuantityTypeIdentifierBasalBodyTemperature} - */ - | 'HKQuantityTypeIdentifierBasalBodyTemperature' - /** - * Blood Pressure Systolic - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbloodpressuresystolic Apple Docs HKQuantityTypeIdentifierBloodPressureSystolic} - */ - | 'HKQuantityTypeIdentifierBloodPressureSystolic' - /** - * Blood Pressure Diastolic - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbloodpressurediastolic Apple Docs HKQuantityTypeIdentifierBloodPressureDiastolic} - */ - | 'HKQuantityTypeIdentifierBloodPressureDiastolic' - /** - * Respiratory Rate - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrespiratoryrate Apple Docs HKQuantityTypeIdentifierRespiratoryRate} - */ - | 'HKQuantityTypeIdentifierRespiratoryRate' - /** - * Resting Heart Rate - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrestingheartrate Apple Docs HKQuantityTypeIdentifierRestingHeartRate} - */ - | 'HKQuantityTypeIdentifierRestingHeartRate' - /** - * Heart Rate Variability SDNN - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierheartratevariabilitysdnn Apple Docs HKQuantityTypeIdentifierHeartRateVariabilitySDNN} - * @since iOS 11.0 - */ - | 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN' - /** - * Oxygen Saturation - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifieroxygensaturation Apple Docs HKQuantityTypeIdentifierOxygenSaturation} - * @since iOS 8.0 - */ - | 'HKQuantityTypeIdentifierOxygenSaturation' - /** - * Peripheral Perfusion Index - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierperipheralperfusionindex Apple Docs HKQuantityTypeIdentifierPeripheralPerfusionIndex} - * @since iOS 8.0 - */ - | 'HKQuantityTypeIdentifierPeripheralPerfusionIndex' - /** - * Blood Glucose - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbloodglucose Apple Docs HKQuantityTypeIdentifierBloodGlucose} - */ - | 'HKQuantityTypeIdentifierBloodGlucose' - - /** - * Blood Ketones - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbloodketones Apple Docs HKQuantityTypeIdentifierBloodKetones} - * @since iOS 8.0 - */ - | 'HKQuantityTypeIdentifierBloodKetones' - - /** - * Number Of Times Fallen - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiernumberoftimesfallen Apple Docs HKQuantityTypeIdentifierNumberOfTimesFallen} - */ - | 'HKQuantityTypeIdentifierNumberOfTimesFallen' - - /** - * Electrodermal Activity - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierelectrodermalactivity Apple Docs HKQuantityTypeIdentifierElectrodermalActivity} - */ - | 'HKQuantityTypeIdentifierElectrodermalActivity' - - /** - * Inhaler Usage - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierinhalerusage Apple Docs HKQuantityTypeIdentifierInhalerUsage} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierInhalerUsage' - - /** - * Insulin Delivery - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierinsulindelivery Apple Docs HKQuantityTypeIdentifierInsulinDelivery} - * @since iOS 11 - */ - | 'HKQuantityTypeIdentifierInsulinDelivery' - - /** - * Blood Alcohol Content - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierbloodalcoholcontent Apple Docs HKQuantityTypeIdentifierBloodAlcoholContent} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierBloodAlcoholContent' - - /** - * Forced Vital Capacity - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierforcedvitalcapacity Apple Docs HKQuantityTypeIdentifierForcedVitalCapacity} - */ - | 'HKQuantityTypeIdentifierForcedVitalCapacity' - - /** - * Forced Expiratory Volume1 - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierforcedexpiratoryvolume1 Apple Docs HKQuantityTypeIdentifierForcedExpiratoryVolume1} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierForcedExpiratoryVolume1' - - /** - * Peak Expiratory Flow Rate - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierpeakexpiratoryflowrate Apple Docs HKQuantityTypeIdentifierPeakExpiratoryFlowRate} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierPeakExpiratoryFlowRate' - - /** - * Environmental Audio Exposure - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierenvironmentalaudioexposure Apple Docs HKQuantityTypeIdentifierEnvironmentalAudioExposure} - * @since iOS 13 - */ - | 'HKQuantityTypeIdentifierEnvironmentalAudioExposure' - - /** - * Headphone Audio Exposure - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierheadphoneaudioexposure Apple Docs HKQuantityTypeIdentifierHeadphoneAudioExposure} - * @since iOS 13 - */ - | 'HKQuantityTypeIdentifierHeadphoneAudioExposure' - - // Nutrition - /** - * Dietary Fat Total - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfattotal Apple Docs HKQuantityTypeIdentifierDietaryFatTotal} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierDietaryFatTotal' - - /** - * Dietary Fat Polyunsaturated - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfatpolyunsaturated Apple Docs HKQuantityTypeIdentifierDietaryFatPolyunsaturated} - */ - | 'HKQuantityTypeIdentifierDietaryFatPolyunsaturated' - - /** - * Dietary Fat Monounsaturated - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfatmonounsaturated Apple Docs HKQuantityTypeIdentifierDietaryFatMonounsaturated} - */ - | 'HKQuantityTypeIdentifierDietaryFatMonounsaturated' - /** - * Dietary Fat Saturated - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfatsaturated Apple Docs HKQuantityTypeIdentifierDietaryFatSaturated} - */ - | 'HKQuantityTypeIdentifierDietaryFatSaturated' - - /** - * Dietary Cholesterol - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarycholesterol Apple Docs HKQuantityTypeIdentifierDietaryCholesterol} - */ - | 'HKQuantityTypeIdentifierDietaryCholesterol' - - /** - * Dietary Sodium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarysodium Apple Docs HKQuantityTypeIdentifierDietarySodium} - */ - | 'HKQuantityTypeIdentifierDietarySodium' - - /** - * Dietary Carbohydrates - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarycarbohydrates Apple Docs HKQuantityTypeIdentifierDietaryCarbohydrates} - */ - | 'HKQuantityTypeIdentifierDietaryCarbohydrates' - - /** - * Dietary Fiber - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfiber Apple Docs HKQuantityTypeIdentifierDietaryFiber} - */ - | 'HKQuantityTypeIdentifierDietaryFiber' - /** - * Dietary Sugar - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarysugar Apple Docs HKQuantityTypeIdentifierDietarySugar} - */ - | 'HKQuantityTypeIdentifierDietarySugar' - - /** - * Dietary Energy Consumed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryenergyconsumed Apple Docs HKQuantityTypeIdentifierDietaryEnergyConsumed} - */ - | 'HKQuantityTypeIdentifierDietaryEnergyConsumed' - - /** - * Dietary Protein - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryprotein Apple Docs HKQuantityTypeIdentifierDietaryProtein} - */ - | 'HKQuantityTypeIdentifierDietaryProtein' - - /** - * Dietary Vitamin A - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitamina Apple Docs HKQuantityTypeIdentifierDietaryVitaminA} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminA' - - /** - * Dietary Vitamin B6 - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitaminb6 Apple Docs HKQuantityTypeIdentifierDietaryVitaminB6} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminB6' - - /** - * Dietary Vitamin B12 - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitaminb12 Apple Docs HKQuantityTypeIdentifierDietaryVitaminB12} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminB12' - - /** - * Dietary Vitamin C - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitaminc Apple Docs HKQuantityTypeIdentifierDietaryVitaminC} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminC' - - /** - * Dietary Vitamin D - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitamind Apple Docs HKQuantityTypeIdentifierDietaryVitaminD} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminD' - - /** - * Dietary Vitamin E - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitamine Apple Docs HKQuantityTypeIdentifierDietaryVitaminE} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminE' - - /** - * Dietary Vitamin K - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryvitamink Apple Docs HKQuantityTypeIdentifierDietaryVitaminK} - */ - | 'HKQuantityTypeIdentifierDietaryVitaminK' - /** - * Dietary Calcium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarycalcium Apple Docs HKQuantityTypeIdentifierDietaryCalcium} - */ - | 'HKQuantityTypeIdentifierDietaryCalcium' - - /** - * Dietary Iron - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryiron Apple Docs HKQuantityTypeIdentifierDietaryIron} - */ - | 'HKQuantityTypeIdentifierDietaryIron' - - /** - * Dietary Thiamin - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarythiamin Apple Docs HKQuantityTypeIdentifierDietaryThiamin} - */ - | 'HKQuantityTypeIdentifierDietaryThiamin' - - /** - * Dietary Riboflavin - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryriboflavin Apple Docs HKQuantityTypeIdentifierDietaryRiboflavin} - */ - | 'HKQuantityTypeIdentifierDietaryRiboflavin' - - /** - * Dietary Niacin - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryniacin Apple Docs HKQuantityTypeIdentifierDietaryNiacin} - */ - | 'HKQuantityTypeIdentifierDietaryNiacin' - - /** - * Dietary Folate - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryfolate Apple Docs HKQuantityTypeIdentifierDietaryFolate} - */ - | 'HKQuantityTypeIdentifierDietaryFolate' - - /** - * Dietary Biotin - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarybiotin Apple Docs HKQuantityTypeIdentifierDietaryBiotin} - */ - | 'HKQuantityTypeIdentifierDietaryBiotin' - - /** - * Dietary Pantothenic Acid - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarypantothenicacid Apple Docs HKQuantityTypeIdentifierDietaryPantothenicAcid} - */ - | 'HKQuantityTypeIdentifierDietaryPantothenicAcid' - - /** - * Dietary Phosphorus - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryphosphorus Apple Docs HKQuantityTypeIdentifierDietaryPhosphorus} - */ - | 'HKQuantityTypeIdentifierDietaryPhosphorus' - - /** - * Dietary Iodine - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryiodine Apple Docs HKQuantityTypeIdentifierDietaryIodine} - */ - | 'HKQuantityTypeIdentifierDietaryIodine' - /** - * Dietary Magnesium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarymagnesium Apple Docs HKQuantityTypeIdentifierDietaryMagnesium} - */ - | 'HKQuantityTypeIdentifierDietaryMagnesium' - - /** - * Dietary Zinc - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryzinc Apple Docs HKQuantityTypeIdentifierDietaryZinc} - */ - | 'HKQuantityTypeIdentifierDietaryZinc' - - /** - * Dietary Selenium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietaryselenium Apple Docs HKQuantityTypeIdentifierDietarySelenium} - */ - | 'HKQuantityTypeIdentifierDietarySelenium' - - /** - * Dietary Copper - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarycopper Apple Docs HKQuantityTypeIdentifierDietaryCopper} - */ - | 'HKQuantityTypeIdentifierDietaryCopper' - - /** - * Dietary Manganese - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarymanganese Apple Docs HKQuantityTypeIdentifierDietaryManganese} - */ - | 'HKQuantityTypeIdentifierDietaryManganese' - - /** - * Dietary Chromium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarychromium Apple Docs HKQuantityTypeIdentifierDietaryChromium} - */ - | 'HKQuantityTypeIdentifierDietaryChromium' - - /** - * Dietary Molybdenum - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarymolybdenum Apple Docs HKQuantityTypeIdentifierDietaryMolybdenum} - */ - | 'HKQuantityTypeIdentifierDietaryMolybdenum' - - /** - * Dietary Chloride - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarychloride Apple Docs HKQuantityTypeIdentifierDietaryChloride} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierDietaryChloride' - - /** - * Dietary Potassium - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarypotassium Apple Docs HKQuantityTypeIdentifierDietaryPotassium} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierDietaryPotassium' - - /** - * Dietary Caffeine - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarycaffeine Apple Docs HKQuantityTypeIdentifierDietaryCaffeine} - * @since iOS 8 - */ - | 'HKQuantityTypeIdentifierDietaryCaffeine' - - /** - * Dietary Water - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierdietarywater Apple Docs HKQuantityTypeIdentifierDietaryWater} - * @since iOS 9 - */ - | 'HKQuantityTypeIdentifierDietaryWater' - - // Mobility - /** - * Six Minute Walk Test Distance - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiersixminutewalktestdistance Apple Docs HKQuantityTypeIdentifierSixMinuteWalkTestDistance} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierSixMinuteWalkTestDistance' - - /** - * Walking Speed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwalkingspeed Apple Docs HKQuantityTypeIdentifierWalkingSpeed} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierWalkingSpeed' - - /** - * Walking Step Length - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwalkingsteplength Apple Docs HKQuantityTypeIdentifierWalkingStepLength} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierWalkingStepLength' - - /** - * Walking Asymmetry Percentage - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwalkingasymmetrypercentage Apple Docs HKQuantityTypeIdentifierWalkingAsymmetryPercentage} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierWalkingAsymmetryPercentage' - - /** - * Walking Double Support Percentage - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwalkingdoublesupportpercentage Apple Docs HKQuantityTypeIdentifierWalkingDoubleSupportPercentage} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierWalkingDoubleSupportPercentage' - - /** - * Stair Ascent Speed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierstairascentspeed Apple Docs HKQuantityTypeIdentifierStairAscentSpeed} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierStairAscentSpeed' - - /** - * Stair Descent Speed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierstairdescentspeed Apple Docs HKQuantityTypeIdentifierStairDescentSpeed} - * @since iOS 14 - */ - | 'HKQuantityTypeIdentifierStairDescentSpeed' - - /** - * UV Exposure - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifieruvexposure Apple Docs HKQuantityTypeIdentifierUVExposure} - * @since iOS 9 - */ - | 'HKQuantityTypeIdentifierUVExposure' // Scalar (Count), Discrete - - /** - * Apple Move Time - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierapplemovetime Apple Docs HKQuantityTypeIdentifierAppleMoveTime} - * @since iOS 14.5 - */ - | 'HKQuantityTypeIdentifierAppleMoveTime' // Time, Cumulative - - /** - * Number Of Alcoholic Beverages - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiernumberofalcoholicbeverages Apple Docs HKQuantityTypeIdentifierNumberOfAlcoholicBeverages} - * @since iOS 15 - */ - | 'HKQuantityTypeIdentifierNumberOfAlcoholicBeverages' // Scalar(Count), Cumulative - - /** - * Underwater Depth - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierunderwaterdepth Apple Docs HKQuantityTypeIdentifierUnderwaterDepth} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierUnderwaterDepth' - - /** - * Water Temperature - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierwatertemperature Apple Docs HKQuantityTypeIdentifierWaterTemperature} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierWaterTemperature' - - /** - * Apple Sleeping Wrist Temperature - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierapplesleepingwristtemperature Apple Docs HKQuantityTypeIdentifierAppleSleepingWristTemperature} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierAppleSleepingWristTemperature' - - /** - * Apple Sleeping Breathing Disturbances - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/applesleepingbreathingdisturbances Apple Docs HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierAppleSleepingBreathingDisturbances' - - /** - * Time In Daylight - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiertimeindaylight Apple Docs HKQuantityTypeIdentifierTimeInDaylight} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierTimeInDaylight' - - /** - * Physical Effort - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierphysicaleffort Apple Docs HKQuantityTypeIdentifierPhysicalEffort} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierPhysicalEffort' - - /** - * Cycling Speed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiercyclingspeed Apple Docs HKQuantityTypeIdentifierCyclingSpeed} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierCyclingSpeed' - - /** - * Cycling Power - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiercyclingpower Apple Docs HKQuantityTypeIdentifierCyclingPower} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierCyclingPower' - - /** - * Cycling Functional Threshold Power - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiercyclingfunctionalthresholdpower Apple Docs HKQuantityTypeIdentifierCyclingFunctionalThresholdPower} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierCyclingFunctionalThresholdPower' - - /** - * Cycling Cadence - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifiercyclingcadence Apple Docs HKQuantityTypeIdentifierCyclingCadence} - * @since iOS 17 - */ - | 'HKQuantityTypeIdentifierCyclingCadence' - - /** - * Environmental Sound Reduction - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierenvironmentalsoundreduction Apple Docs HKQuantityTypeIdentifierEnvironmentalSoundReduction} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierEnvironmentalSoundReduction' - - /** - * Heart Rate Recovery One Minute - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierheartraterecoveryoneminute Apple Docs HKQuantityTypeIdentifierHeartRateRecoveryOneMinute} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierHeartRateRecoveryOneMinute' - - /** - * Running Ground Contact Time - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrunninggroundcontacttime Apple Docs HKQuantityTypeIdentifierRunningGroundContactTime} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierRunningGroundContactTime' - - /** - * Running Stride Length - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrunningstridelength Apple Docs HKQuantityTypeIdentifierRunningStrideLength} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierRunningStrideLength' - - /** - * Running Power - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrunningpower Apple Docs HKQuantityTypeIdentifierRunningPower} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierRunningPower' - - /** - * Running Vertical Oscillation - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrunningverticaloscillation Apple Docs HKQuantityTypeIdentifierRunningVerticalOscillation} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierRunningVerticalOscillation' - - /** - * Running Speed - * @see {@link https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifierrunningspeed Apple Docs HKQuantityTypeIdentifierRunningSpeed} - * @since iOS 16 - */ - | 'HKQuantityTypeIdentifierRunningSpeed' - - /** - * Cross Country Skiing Speed - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierCrossCountrySkiingSpeed Apple Docs HKQuantityTypeIdentifierCrossCountrySkiingSpeed} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierCrossCountrySkiingSpeed' - - /** - * Cross Country Skiing Distance - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierDistanceCrossCountrySkiing Apple Docs HKQuantityTypeIdentifierCrossCountrySkiingDistance} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierDistanceCrossCountrySkiing' - - /** - * Paddle Sports Distance - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierDistancePaddleSports Apple Docs HKQuantityTypeIdentifierDistancePaddleSports} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierDistancePaddleSports' - - /** - * Rowing Distance - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierDistanceRowing Apple Docs HKQuantityTypeIdentifierDistanceRowing} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierDistanceRowing' - - /** - * Skating Sports Distance - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierDistanceSkatingSports Apple Docs HKQuantityTypeIdentifierDistanceSkatingSports} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierDistanceSkatingSports' - - /** - * Estimated Workout Effort Score - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierEstimatedWorkoutEffortScore Apple Docs HKQuantityTypeIdentifierEstimatedWorkoutEffortScore} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierEstimatedWorkoutEffortScore' - - /** - * Paddle Sports Speed - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierPaddleSportsSpeed Apple Docs HKQuantityTypeIdentifierPaddleSportsSpeed} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierPaddleSportsSpeed' - - /** - * Rowing Speed - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierRowingSpeed Apple Docs HKQuantityTypeIdentifierRowingSpeed} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierRowingSpeed' - - /** - * Workout Effort Score - * @see {@link https://developer.apple.com/documentation/healthkit/HKQuantityTypeIdentifierWorkoutEffortScore Apple Docs HKQuantityTypeIdentifierWorkoutEffortScore} - * @since iOS 18 - */ - | 'HKQuantityTypeIdentifierWorkoutEffortScore' - -export type QuantityTypeIdentifier = - | QuantityTypeIdentifierReadOnly - | QuantityTypeIdentifierWriteable +export type { + QuantityTypeIdentifier, + QuantityTypeIdentifierReadOnly, + QuantityTypeIdentifierWriteable, +} from '../generated/healthkit.generated' diff --git a/packages/react-native-healthkit/src/types/QueryOptions.ts b/packages/react-native-healthkit/src/types/QueryOptions.ts index c054506b..c1af55d3 100644 --- a/packages/react-native-healthkit/src/types/QueryOptions.ts +++ b/packages/react-native-healthkit/src/types/QueryOptions.ts @@ -102,11 +102,18 @@ export interface QueryOptionsWithSortOrder extends GenericQueryOptions { readonly ascending?: boolean } -export interface QueryOptionsWithSortOrderAndUnit +export interface QueryOptionsWithSortOrderAndUnit extends QueryOptionsWithSortOrder { - readonly unit?: string + readonly unit?: TUnit } -export interface QueryOptionsWithAnchorAndUnit extends QueryOptionsWithAnchor { - readonly unit?: string +export interface QueryOptionsWithAnchorAndUnit + extends QueryOptionsWithAnchor { + readonly unit?: TUnit } + +export interface QueryOptionsWithSortOrderAndStringUnit + extends QueryOptionsWithSortOrderAndUnit {} + +export interface QueryOptionsWithAnchorAndStringUnit + extends QueryOptionsWithAnchorAndUnit {} diff --git a/packages/react-native-healthkit/src/types/Shared.ts b/packages/react-native-healthkit/src/types/Shared.ts index fd132294..6cfc237d 100644 --- a/packages/react-native-healthkit/src/types/Shared.ts +++ b/packages/react-native-healthkit/src/types/Shared.ts @@ -1,4 +1,8 @@ import type { AnyMap } from 'react-native-nitro-modules' +import type { + KnownObjectMetadata, + KnownSampleMetadata, +} from '../generated/healthkit.generated' import type { CategoryTypeIdentifier, CategoryTypeIdentifierWriteable, @@ -17,17 +21,11 @@ import type { } from './Constants' import type { CorrelationTypeIdentifier } from './CorrelationType' import type { Device } from './Device' -import type { - HeartRateMotionContext, - InsulinDeliveryReason, - Quantity, -} from './QuantityType' import type { QuantityTypeIdentifier, QuantityTypeIdentifierWriteable, } from './QuantityTypeIdentifier' import type { SourceRevision } from './Source' -import type { WeatherCondition } from './WeatherCondition' export interface DeletedSample { readonly uuid: string @@ -64,45 +62,30 @@ export type SampleTypeIdentifierWriteable = | typeof WorkoutRouteTypeIdentifier | typeof WorkoutTypeIdentifier -export interface GenericMetadata { - readonly HKExternalUUID?: string - readonly HKTimeZone?: string - readonly HKWasUserEntered?: boolean - readonly HKDeviceSerialNumber?: string - readonly HKUDIDeviceIdentifier?: string - readonly HKUDIProductionIdentifier?: string - readonly HKDigitalSignature?: string - readonly HKDeviceName?: string - readonly HKDeviceManufacturerName?: string - readonly HKSyncIdentifier?: string - readonly HKSyncVersion?: number - readonly HKWasTakenInLab?: boolean - readonly HKReferenceRangeLowerLimit?: number - readonly HKReferenceRangeUpperLimit?: number +export type MetadataWithUnknown = AnyMap & T +export type GenericMetadata = AnyMap & KnownObjectMetadata +export type WithTypedMetadata = Omit< + T, + 'metadata' +> & { + readonly metadata: AnyMap & TMetadata +} +export type WithOptionalTypedMetadata = Omit< + T, + 'metadata' +> & { + readonly metadata?: AnyMap & TMetadata } +export type BaseObjectTyped = + WithTypedMetadata +export type BaseSampleTyped = + WithTypedMetadata export interface BaseObject { readonly uuid: string readonly sourceRevision: SourceRevision readonly device?: Device readonly metadata: AnyMap - - // metadata - readonly metadataExternalUUID?: string - readonly metadataTimeZone?: string - readonly metadataWasUserEntered?: boolean - readonly metadataDeviceSerialNumber?: string - readonly metadataUdiDeviceIdentifier?: string - readonly metadataUdiProductionIdentifier?: string - readonly metadataDigitalSignature?: string - readonly metadataDeviceName?: string - readonly metadataDeviceManufacturerName?: string - readonly metadataSyncIdentifier?: string - readonly metadataSyncVersion?: number - readonly metadataWasTakenInLab?: boolean - readonly metadataReferenceRangeLowerLimit?: number - readonly metadataReferenceRangeUpperLimit?: number - readonly metadataAlgorithmVersion?: number } export interface SampleType { @@ -117,15 +100,5 @@ export interface BaseSample extends BaseObject { readonly startDate: Date readonly endDate: Date readonly hasUndeterminedDuration: boolean - - // metadata - readonly metadataWeatherCondition?: WeatherCondition - readonly metadataWeatherHumidity?: Quantity - readonly metadataWeatherTemperature?: Quantity - readonly metadataInsulinDeliveryReason?: InsulinDeliveryReason - /** - * postprandial or preprandial (https://developer.apple.com/documentation/healthkit/hkbloodglucosemealtime) - */ - // readonly metadataBloodGlucoseMealTime?: number - readonly metadataHeartRateMotionContext?: HeartRateMotionContext + readonly metadata: AnyMap } diff --git a/packages/react-native-healthkit/src/types/StateOfMind.ts b/packages/react-native-healthkit/src/types/StateOfMind.ts index 177e7343..4c5fcbbc 100644 --- a/packages/react-native-healthkit/src/types/StateOfMind.ts +++ b/packages/react-native-healthkit/src/types/StateOfMind.ts @@ -1,4 +1,5 @@ -import type { BaseSample, DeletedSample } from './Shared' +import type { KnownSampleMetadata } from '../generated/healthkit.generated' +import type { BaseSample, DeletedSample, WithTypedMetadata } from './Shared' export enum StateOfMindValenceClassification { veryUnpleasant = 1, @@ -23,12 +24,23 @@ export interface StateOfMindSample extends BaseSample { readonly labels: readonly StateOfMindLabel[] } +export type StateOfMindSampleTyped = WithTypedMetadata< + StateOfMindSample, + KnownSampleMetadata +> + export interface StateOfMindSamplesWithAnchorResponse { readonly samples: readonly StateOfMindSample[] readonly deletedSamples: readonly DeletedSample[] readonly newAnchor: string } +export interface StateOfMindSamplesWithAnchorResponseTyped { + readonly samples: readonly StateOfMindSampleTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} + /** * @see {@link https://developer.apple.com/documentation/healthkit/hkstateofmind/label Apple Docs} */ diff --git a/packages/react-native-healthkit/src/types/Subscriptions.ts b/packages/react-native-healthkit/src/types/Subscriptions.ts index 9421090d..7bd214f8 100644 --- a/packages/react-native-healthkit/src/types/Subscriptions.ts +++ b/packages/react-native-healthkit/src/types/Subscriptions.ts @@ -1,6 +1,6 @@ import type { CategorySampleTyped } from './CategoryType' import type { CategoryTypeIdentifier } from './CategoryTypeIdentifier' -import type { QuantitySample } from './QuantitySample' +import type { QuantitySampleTyped } from './QuantitySample' import type { QuantityTypeIdentifier } from './QuantityTypeIdentifier' import type { SampleTypeIdentifier } from './Shared' @@ -13,32 +13,38 @@ export interface OnChangeCallbackArgs { readonly errorMessage?: string } -export interface OnQuantitySamplesCallbackError { - readonly typeIdentifier: QuantityTypeIdentifier +export interface OnQuantitySamplesCallbackError< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> { + readonly typeIdentifier: T readonly errorMessage: string } -export interface OnQuantitySamplesCallbackSuccess { - readonly typeIdentifier: QuantityTypeIdentifier - readonly samples: readonly QuantitySample[] +export interface OnQuantitySamplesCallbackSuccess< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> { + readonly typeIdentifier: T + readonly samples: readonly QuantitySampleTyped[] } -export type OnQuantitySamplesCallback = - | OnQuantitySamplesCallbackError - | OnQuantitySamplesCallbackSuccess +export type OnQuantitySamplesCallback< + T extends QuantityTypeIdentifier = QuantityTypeIdentifier, +> = OnQuantitySamplesCallbackError | OnQuantitySamplesCallbackSuccess -export interface OnCategorySamplesCallbackError { - readonly typeIdentifier: CategoryTypeIdentifier +export interface OnCategorySamplesCallbackError< + T extends CategoryTypeIdentifier = CategoryTypeIdentifier, +> { + readonly typeIdentifier: T readonly errorMessage: string } export interface OnCategorySamplesCallbackSuccess< T extends CategoryTypeIdentifier, > { - readonly typeIdentifier: CategoryTypeIdentifier + readonly typeIdentifier: T readonly samples: readonly CategorySampleTyped[] } export type OnCategorySamplesCallback = - | OnCategorySamplesCallbackError + | OnCategorySamplesCallbackError | OnCategorySamplesCallbackSuccess diff --git a/packages/react-native-healthkit/src/types/WeatherCondition.ts b/packages/react-native-healthkit/src/types/WeatherCondition.ts index b13011ee..5d921b2e 100644 --- a/packages/react-native-healthkit/src/types/WeatherCondition.ts +++ b/packages/react-native-healthkit/src/types/WeatherCondition.ts @@ -1,31 +1 @@ -// documented at https://developer.apple.com/documentation/healthkit/hkweathercondition -export enum WeatherCondition { - none = 0, - clear = 1, - fair = 2, - partlyCloudy = 3, - mostlyCloudy = 4, - cloudy = 5, - foggy = 6, - haze = 7, - windy = 8, - blustery = 9, - smoky = 10, - dust = 11, - snow = 12, - hail = 13, - sleet = 14, - freezingDrizzle = 15, - freezingRain = 16, - mixedRainAndHail = 17, - mixedRainAndSnow = 18, - mixedRainAndSleet = 19, - mixedSnowAndSleet = 20, - drizzle = 21, - scatteredShowers = 22, - showers = 23, - thunderstorms = 24, - tropicalStorm = 25, - hurricane = 26, - tornado = 27, -} +export { WeatherCondition } from '../generated/healthkit.generated' diff --git a/packages/react-native-healthkit/src/types/Workouts.ts b/packages/react-native-healthkit/src/types/Workouts.ts index 2144acff..b10ee076 100644 --- a/packages/react-native-healthkit/src/types/Workouts.ts +++ b/packages/react-native-healthkit/src/types/Workouts.ts @@ -1,96 +1,23 @@ import type { AnyMap } from 'react-native-nitro-modules' +import type { + WorkoutEventTypedMetadata, + WorkoutTypedMetadata, +} from '../generated/healthkit.generated' +import { + WorkoutActivityType, + WorkoutEventType, +} from '../generated/healthkit.generated' import type { WorkoutProxy } from '../specs/WorkoutProxy.nitro' import type { BaseSample, ComparisonPredicateOperator } from '../types' import type { Quantity } from './QuantityType' import type { FilterForSamplesBase } from './QueryOptions' -import type { DeletedSample } from './Shared' - -export enum WorkoutActivityType { - americanFootball = 1, - archery = 2, - australianFootball = 3, - badminton = 4, - baseball = 5, - basketball = 6, - bowling = 7, - boxing = 8, // See also HKWorkoutActivityTypeKickboxing., - climbing = 9, - cricket = 10, - crossTraining = 11, // Any mix of cardio and/or strength training. See also HKWorkoutActivityTypeCoreTraining and HKWorkoutActivityTypeFlexibility., - curling = 12, - cycling = 13, - dance = 14, - danceInspiredTraining = 15, // This enum remains available to access older data., - elliptical = 16, - equestrianSports = 17, // Polo, Horse Racing, Horse Riding, etc., - fencing = 18, - fishing = 19, - functionalStrengthTraining = 20, // Primarily free weights and/or body weight and/or accessories, - golf = 21, - gymnastics = 22, - handball = 23, - hiking = 24, - hockey = 25, // Ice Hockey, Field Hockey, etc., - hunting = 26, - lacrosse = 27, - martialArts = 28, - mindAndBody = 29, // Qigong, meditation, etc., - mixedMetabolicCardioTraining = 30, // This enum remains available to access older data., - paddleSports = 31, // Canoeing, Kayaking, Outrigger, Stand Up Paddle Board, etc., - play = 32, // Dodge Ball, Hopscotch, Tetherball, Jungle Gym, etc., - preparationAndRecovery = 33, // Foam rolling, stretching, etc., - racquetball = 34, - rowing = 35, - rugby = 36, - running = 37, - sailing = 38, - skatingSports = 39, // Ice Skating, Speed Skating, Inline Skating, Skateboarding, etc., - snowSports = 40, // Sledding, Snowmobiling, Building a Snowman, etc. See also HKWorkoutActivityTypeCrossCountrySkiing, HKWorkoutActivityTypeSnowboarding, and HKWorkoutActivityTypeDownhillSkiing., - soccer = 41, - softball = 42, - squash = 43, - stairClimbing = 44, // See also HKWorkoutActivityTypeStairs and HKWorkoutActivityTypeStepTraining., - surfingSports = 45, // Traditional Surfing, Kite Surfing, Wind Surfing, etc., - swimming = 46, - tableTennis = 47, - tennis = 48, - trackAndField = 49, // Shot Put, Javelin, Pole Vaulting, etc., - traditionalStrengthTraining = 50, // Primarily machines and/or free weights, - volleyball = 51, - walking = 52, - waterFitness = 53, - waterPolo = 54, - waterSports = 55, // Water Skiing, Wake Boarding, etc., - wrestling = 56, - yoga = 57, - barre = 58, // HKWorkoutActivityTypeDanceInspiredTraining, - coreTraining = 59, - crossCountrySkiing = 60, - downhillSkiing = 61, - flexibility = 62, - highIntensityIntervalTraining = 63, - jumpRope = 64, - kickboxing = 65, - pilates = 66, // HKWorkoutActivityTypeDanceInspiredTraining, - snowboarding = 67, - stairs = 68, - stepTraining = 69, - wheelchairWalkPace = 70, - wheelchairRunPace = 71, - taiChi = 72, - mixedCardio = 73, // HKWorkoutActivityTypeMixedMetabolicCardioTraining, - handCycling = 74, - discSports = 75, - fitnessGaming = 76, - cardioDance = 77, - socialDance = 78, - pickleball = 79, - cooldown = 80, - swimBikeRun = 82, - transition = 83, - underwaterDiving = 84, - other = 3000, -} +import type { + DeletedSample, + WithOptionalTypedMetadata, + WithTypedMetadata, +} from './Shared' + +export { WorkoutActivityType, WorkoutEventType } export interface WorkoutEvent { readonly type: WorkoutEventType @@ -99,16 +26,10 @@ export interface WorkoutEvent { readonly metadata?: AnyMap } -export enum WorkoutEventType { - pause = 1, - resume = 2, - lap = 3, - marker = 4, - motionPaused = 5, - motionResumed = 6, - segment = 7, - pauseOrResumeRequest = 8, -} +export type WorkoutEventTyped = WithOptionalTypedMetadata< + WorkoutEvent, + WorkoutEventTypedMetadata +> export interface WorkoutActivity { readonly startDate: Date @@ -129,6 +50,12 @@ export interface QueryWorkoutSamplesWithAnchorResponse { readonly newAnchor: string } +export interface QueryWorkoutSamplesWithAnchorResponseTyped { + readonly workouts: readonly WorkoutProxyTyped[] + readonly deletedSamples: readonly DeletedSample[] + readonly newAnchor: string +} + export type WorkoutDurationPredicate = { readonly predicateOperator: ComparisonPredicateOperator readonly durationInSeconds: number @@ -180,7 +107,7 @@ export interface WorkoutRouteLocation { export interface LocationForSaving { readonly altitude: number readonly course: number - readonly date: Date // unix timestamp in milliseconds + readonly date: Date readonly horizontalAccuracy: number readonly latitude: number readonly longitude: number @@ -198,7 +125,7 @@ export interface WorkoutTotals { readonly energyBurned?: number } -export interface WorkoutSample extends BaseSample { +export interface WorkoutSample extends Omit { readonly workoutActivityType: WorkoutActivityType readonly duration: Quantity readonly totalEnergyBurned?: Quantity @@ -207,11 +134,20 @@ export interface WorkoutSample extends BaseSample { readonly totalFlightsClimbed?: Quantity readonly events?: readonly WorkoutEvent[] readonly activities?: readonly WorkoutActivity[] - - readonly metadataAverageMETs?: Quantity - readonly metadataElevationAscended?: Quantity - readonly metadataElevationDescended?: Quantity - readonly metadataIndoorWorkout?: boolean - readonly metadataAverageSpeed?: Quantity - readonly metadataMaximumSpeed?: Quantity -} + readonly metadata: AnyMap +} + +export type WorkoutSampleTyped = WithTypedMetadata< + Omit & { + readonly events?: readonly WorkoutEventTyped[] + }, + WorkoutTypedMetadata +> + +export type WorkoutProxyTyped = Omit< + WorkoutProxy, + keyof WorkoutSample | 'toJSON' +> & + WorkoutSampleTyped & { + toJSON(key?: string): WorkoutSampleTyped + } diff --git a/packages/react-native-healthkit/src/types/index.ts b/packages/react-native-healthkit/src/types/index.ts index d8439de3..688c5c64 100644 --- a/packages/react-native-healthkit/src/types/index.ts +++ b/packages/react-native-healthkit/src/types/index.ts @@ -7,6 +7,8 @@ export * from './Constants' export * from './CorrelationType' export * from './Device' export * from './HeartbeatSeries' +export * from './Medication' +export * from './MetadataEnums' export * from './QuantitySample' export * from './QuantityType' export * from './QuantityTypeIdentifier' diff --git a/packages/react-native-healthkit/src/utils/getMostRecentQuantitySample.ts b/packages/react-native-healthkit/src/utils/getMostRecentQuantitySample.ts index 0abc4e2b..43bf37dc 100644 --- a/packages/react-native-healthkit/src/utils/getMostRecentQuantitySample.ts +++ b/packages/react-native-healthkit/src/utils/getMostRecentQuantitySample.ts @@ -1,9 +1,10 @@ import { QuantityTypes } from '../modules' +import type { UnitForIdentifier } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' -async function getMostRecentQuantitySample( - identifier: QuantityTypeIdentifier, - unit?: string, +async function getMostRecentQuantitySample( + identifier: T, + unit?: UnitForIdentifier, ) { const samples = await QuantityTypes.queryQuantitySamples(identifier, { limit: 1, diff --git a/packages/react-native-healthkit/src/utils/getMostRecentWorkout.ts b/packages/react-native-healthkit/src/utils/getMostRecentWorkout.ts index 02a24f08..7cadae2d 100644 --- a/packages/react-native-healthkit/src/utils/getMostRecentWorkout.ts +++ b/packages/react-native-healthkit/src/utils/getMostRecentWorkout.ts @@ -1,12 +1,15 @@ import { Workouts } from '../modules' +import type { WorkoutProxyTyped } from '../types/Workouts' -const getMostRecentWorkout = async () => { +const getMostRecentWorkout = async (): Promise< + WorkoutProxyTyped | undefined +> => { const workouts = await Workouts.queryWorkoutSamples({ limit: 1, ascending: false, }) - return workouts[0] + return workouts[0] as WorkoutProxyTyped | undefined } export default getMostRecentWorkout diff --git a/packages/react-native-healthkit/src/utils/getPreferredUnit.ts b/packages/react-native-healthkit/src/utils/getPreferredUnit.ts index f3ff76b4..11ecf603 100644 --- a/packages/react-native-healthkit/src/utils/getPreferredUnit.ts +++ b/packages/react-native-healthkit/src/utils/getPreferredUnit.ts @@ -1,9 +1,10 @@ import { Core } from '../modules' +import type { UnitForIdentifier } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' -const getPreferredUnit = async ( - quantityType: QuantityTypeIdentifier, -): Promise => { +const getPreferredUnit = async ( + quantityType: T, +): Promise> => { const units = await Core.getPreferredUnits([quantityType]) const unit = units[0]?.unit @@ -13,6 +14,6 @@ const getPreferredUnit = async ( ) } - return unit + return unit as UnitForIdentifier } export default getPreferredUnit diff --git a/packages/react-native-healthkit/src/utils/getQuantitySampleById.ts b/packages/react-native-healthkit/src/utils/getQuantitySampleById.ts index 19c08f94..2f423134 100644 --- a/packages/react-native-healthkit/src/utils/getQuantitySampleById.ts +++ b/packages/react-native-healthkit/src/utils/getQuantitySampleById.ts @@ -1,10 +1,11 @@ import { QuantityTypes } from '../modules' +import type { UnitForIdentifier } from '../types/QuantityType' import type { QuantityTypeIdentifier } from '../types/QuantityTypeIdentifier' -async function getQuantitySampleById( - identifier: QuantityTypeIdentifier, +async function getQuantitySampleById( + identifier: T, uuid: string, - unit?: string, + unit?: UnitForIdentifier, ) { const samples = await QuantityTypes.queryQuantitySamples(identifier, { limit: 1, diff --git a/packages/react-native-healthkit/src/utils/subscribeToQuantitySamples.ts b/packages/react-native-healthkit/src/utils/subscribeToQuantitySamples.ts index b0796eba..b2052732 100644 --- a/packages/react-native-healthkit/src/utils/subscribeToQuantitySamples.ts +++ b/packages/react-native-healthkit/src/utils/subscribeToQuantitySamples.ts @@ -3,9 +3,11 @@ import type { QuantityTypeIdentifier } from '../types' import type { OnQuantitySamplesCallback } from '../types/Subscriptions' import { subscribeToChanges } from './subscribeToChanges' -export const subscribeToQuantitySamples = ( - identifier: QuantityTypeIdentifier, - callback: (args: OnQuantitySamplesCallback) => void, +export const subscribeToQuantitySamples = < + TIdentifier extends QuantityTypeIdentifier, +>( + identifier: TIdentifier, + callback: (args: OnQuantitySamplesCallback) => void, after = new Date(), ) => { return subscribeToChanges(identifier, async ({ errorMessage }) => { diff --git a/packages/react-native-healthkit/tsconfig.type-tests.json b/packages/react-native-healthkit/tsconfig.type-tests.json new file mode 100644 index 00000000..0fa10ef8 --- /dev/null +++ b/packages/react-native-healthkit/tsconfig.type-tests.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/type-tests/**/*.ts"], + "exclude": [], + "compilerOptions": { + "noEmit": true + } +}