@peerbits/fhir-observation-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Respiratory Rate Observation Generator.
3
+ */
4
+ import { LOINC_MAP, LOINC_SYSTEM, UCUM_SYSTEM, formatCategory } from "../loinc-map.js";
5
+ import { convertUnit } from "../units.js";
6
+ import { validateObservation } from "../validate.js";
7
+ export function respiratoryRateToObservation(reading, config) {
8
+ if (reading.deviceType !== "respiratory-rate") {
9
+ throw new Error(`Invalid deviceType for respiratory rate generator: expected 'respiratory-rate', got '${reading.deviceType}'`);
10
+ }
11
+ if (typeof reading.value !== "number") {
12
+ throw new Error(`Invalid respiratory rate value: expected number, got ${typeof reading.value}`);
13
+ }
14
+ const mapping = LOINC_MAP["respiratory-rate"];
15
+ const targetUnit = config?.targetUnit || mapping.defaultUcumUnit;
16
+ const conv = convertUnit(reading.value, reading.unit, targetUnit);
17
+ const observation = {
18
+ resourceType: "Observation",
19
+ status: config?.status || "final",
20
+ category: formatCategory(mapping.category),
21
+ code: {
22
+ coding: [
23
+ {
24
+ system: LOINC_SYSTEM,
25
+ code: mapping.loincCode,
26
+ display: mapping.display,
27
+ },
28
+ ],
29
+ text: mapping.display,
30
+ },
31
+ subject: {
32
+ reference: reading.patientRef,
33
+ },
34
+ effectiveDateTime: reading.timestamp,
35
+ ...(reading.deviceId ? { device: { reference: reading.deviceId } } : {}),
36
+ valueQuantity: {
37
+ value: conv.value,
38
+ unit: conv.unit,
39
+ system: UCUM_SYSTEM,
40
+ code: conv.ucumCode,
41
+ },
42
+ };
43
+ if (config?.validate) {
44
+ const result = validateObservation(observation);
45
+ if (!result.valid) {
46
+ throw new Error(`Structural validation failed for respiratory rate observation: ${result.errors.join(", ")}`);
47
+ }
48
+ }
49
+ return observation;
50
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SpO2 (Oxygen Saturation) Observation Generator.
3
+ */
4
+ import { DeviceReading, FhirObservation, GeneratorConfig } from "../types.js";
5
+ export declare function spo2ToObservation(reading: DeviceReading, config?: GeneratorConfig): FhirObservation;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SpO2 (Oxygen Saturation) Observation Generator.
3
+ */
4
+ import { LOINC_MAP, LOINC_SYSTEM, UCUM_SYSTEM, formatCategory } from "../loinc-map.js";
5
+ import { convertUnit } from "../units.js";
6
+ import { validateObservation } from "../validate.js";
7
+ export function spo2ToObservation(reading, config) {
8
+ if (reading.deviceType !== "spo2") {
9
+ throw new Error(`Invalid deviceType for spo2 generator: expected 'spo2', got '${reading.deviceType}'`);
10
+ }
11
+ if (typeof reading.value !== "number") {
12
+ throw new Error(`Invalid spo2 value: expected number, got ${typeof reading.value}`);
13
+ }
14
+ const mapping = LOINC_MAP["spo2"];
15
+ const targetUnit = config?.targetUnit || mapping.defaultUcumUnit;
16
+ const conv = convertUnit(reading.value, reading.unit, targetUnit);
17
+ const observation = {
18
+ resourceType: "Observation",
19
+ status: config?.status || "final",
20
+ category: formatCategory(mapping.category),
21
+ code: {
22
+ coding: [
23
+ {
24
+ system: LOINC_SYSTEM,
25
+ code: mapping.loincCode,
26
+ display: mapping.display,
27
+ },
28
+ ],
29
+ text: mapping.display,
30
+ },
31
+ subject: {
32
+ reference: reading.patientRef,
33
+ },
34
+ effectiveDateTime: reading.timestamp,
35
+ ...(reading.deviceId ? { device: { reference: reading.deviceId } } : {}),
36
+ valueQuantity: {
37
+ value: conv.value,
38
+ unit: conv.unit,
39
+ system: UCUM_SYSTEM,
40
+ code: conv.ucumCode,
41
+ },
42
+ };
43
+ if (config?.validate) {
44
+ const result = validateObservation(observation);
45
+ if (!result.valid) {
46
+ throw new Error(`Structural validation failed for SpO2 observation: ${result.errors.join(", ")}`);
47
+ }
48
+ }
49
+ return observation;
50
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Body Temperature Observation Generator.
3
+ */
4
+ import { DeviceReading, FhirObservation, GeneratorConfig } from "../types.js";
5
+ export declare function temperatureToObservation(reading: DeviceReading, config?: GeneratorConfig): FhirObservation;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Body Temperature Observation Generator.
3
+ */
4
+ import { LOINC_MAP, LOINC_SYSTEM, UCUM_SYSTEM, formatCategory } from "../loinc-map.js";
5
+ import { convertUnit } from "../units.js";
6
+ import { validateObservation } from "../validate.js";
7
+ export function temperatureToObservation(reading, config) {
8
+ if (reading.deviceType !== "temperature") {
9
+ throw new Error(`Invalid deviceType for temperature generator: expected 'temperature', got '${reading.deviceType}'`);
10
+ }
11
+ if (typeof reading.value !== "number") {
12
+ throw new Error(`Invalid temperature value: expected number, got ${typeof reading.value}`);
13
+ }
14
+ const mapping = LOINC_MAP["temperature"];
15
+ const targetUnit = config?.targetUnit || mapping.defaultUcumUnit;
16
+ const conv = convertUnit(reading.value, reading.unit, targetUnit);
17
+ const observation = {
18
+ resourceType: "Observation",
19
+ status: config?.status || "final",
20
+ category: formatCategory(mapping.category),
21
+ code: {
22
+ coding: [
23
+ {
24
+ system: LOINC_SYSTEM,
25
+ code: mapping.loincCode,
26
+ display: mapping.display,
27
+ },
28
+ ],
29
+ text: mapping.display,
30
+ },
31
+ subject: {
32
+ reference: reading.patientRef,
33
+ },
34
+ effectiveDateTime: reading.timestamp,
35
+ ...(reading.deviceId ? { device: { reference: reading.deviceId } } : {}),
36
+ valueQuantity: {
37
+ value: conv.value,
38
+ unit: conv.unit,
39
+ system: UCUM_SYSTEM,
40
+ code: conv.ucumCode,
41
+ },
42
+ };
43
+ if (config?.validate) {
44
+ const result = validateObservation(observation);
45
+ if (!result.valid) {
46
+ throw new Error(`Structural validation failed for temperature observation: ${result.errors.join(", ")}`);
47
+ }
48
+ }
49
+ return observation;
50
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Weight Observation Generator.
3
+ */
4
+ import { DeviceReading, FhirObservation, GeneratorConfig } from "../types.js";
5
+ export declare function weightToObservation(reading: DeviceReading, config?: GeneratorConfig): FhirObservation;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Weight Observation Generator.
3
+ */
4
+ import { LOINC_MAP, LOINC_SYSTEM, UCUM_SYSTEM, formatCategory } from "../loinc-map.js";
5
+ import { convertUnit } from "../units.js";
6
+ import { validateObservation } from "../validate.js";
7
+ export function weightToObservation(reading, config) {
8
+ if (reading.deviceType !== "weight") {
9
+ throw new Error(`Invalid deviceType for weight generator: expected 'weight', got '${reading.deviceType}'`);
10
+ }
11
+ if (typeof reading.value !== "number") {
12
+ throw new Error(`Invalid weight value: expected number, got ${typeof reading.value}`);
13
+ }
14
+ const mapping = LOINC_MAP["weight"];
15
+ const targetUnit = config?.targetUnit || mapping.defaultUcumUnit;
16
+ const conv = convertUnit(reading.value, reading.unit, targetUnit);
17
+ const observation = {
18
+ resourceType: "Observation",
19
+ status: config?.status || "final",
20
+ category: formatCategory(mapping.category),
21
+ code: {
22
+ coding: [
23
+ {
24
+ system: LOINC_SYSTEM,
25
+ code: mapping.loincCode,
26
+ display: mapping.display,
27
+ },
28
+ ],
29
+ text: mapping.display,
30
+ },
31
+ subject: {
32
+ reference: reading.patientRef,
33
+ },
34
+ effectiveDateTime: reading.timestamp,
35
+ ...(reading.deviceId ? { device: { reference: reading.deviceId } } : {}),
36
+ valueQuantity: {
37
+ value: conv.value,
38
+ unit: conv.unit,
39
+ system: UCUM_SYSTEM,
40
+ code: conv.ucumCode,
41
+ },
42
+ };
43
+ if (config?.validate) {
44
+ const result = validateObservation(observation);
45
+ if (!result.valid) {
46
+ throw new Error(`Structural validation failed for weight observation: ${result.errors.join(", ")}`);
47
+ }
48
+ }
49
+ return observation;
50
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @peerbits/fhir-observation-generator
3
+ *
4
+ * Pure data-transformation library for generating FHIR Observation resources
5
+ * from Remote Patient Monitoring (RPM) device readings with LOINC coding and unit conversion.
6
+ */
7
+ export * from "./types.js";
8
+ export * from "./loinc-map.js";
9
+ export * from "./units.js";
10
+ export * from "./validate.js";
11
+ export * from "./bundle.js";
12
+ export * from "./generators/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @peerbits/fhir-observation-generator
3
+ *
4
+ * Pure data-transformation library for generating FHIR Observation resources
5
+ * from Remote Patient Monitoring (RPM) device readings with LOINC coding and unit conversion.
6
+ */
7
+ export * from "./types.js";
8
+ export * from "./loinc-map.js";
9
+ export * from "./units.js";
10
+ export * from "./validate.js";
11
+ export * from "./bundle.js";
12
+ export * from "./generators/index.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Single source of truth mapping each deviceType to its official LOINC code,
3
+ * display text, FHIR Observation category, and standard UCUM target unit.
4
+ *
5
+ * Grounded in the official LOINC database (loinc.org) and US Core Vital Signs Profile.
6
+ */
7
+ import { DeviceType, FhirCodeableConcept } from "./types.js";
8
+ export interface LoincMapping {
9
+ deviceType: DeviceType;
10
+ loincCode: string;
11
+ display: string;
12
+ category: {
13
+ system: string;
14
+ code: string;
15
+ display: string;
16
+ };
17
+ defaultUcumUnit: string;
18
+ defaultUnitDisplay: string;
19
+ }
20
+ export interface ComponentLoincMapping {
21
+ loincCode: string;
22
+ display: string;
23
+ ucumUnit: string;
24
+ unitDisplay: string;
25
+ }
26
+ export declare const VITAL_SIGNS_CATEGORY: {
27
+ system: string;
28
+ code: string;
29
+ display: string;
30
+ };
31
+ export declare const LOINC_SYSTEM = "http://loinc.org";
32
+ export declare const UCUM_SYSTEM = "http://unitsofmeasure.org";
33
+ export declare const LOINC_MAP: Record<DeviceType, LoincMapping>;
34
+ /**
35
+ * Component LOINC mappings for blood pressure components.
36
+ */
37
+ export declare const LOINC_BP_SYSTOLIC: ComponentLoincMapping;
38
+ export declare const LOINC_BP_DIASTOLIC: ComponentLoincMapping;
39
+ /**
40
+ * Helper function to retrieve LOINC mapping for a deviceType.
41
+ */
42
+ export declare function getLoincMapping(deviceType: DeviceType): LoincMapping;
43
+ /**
44
+ * Formats a Category object into a FHIR CodeableConcept array.
45
+ */
46
+ export declare function formatCategory(category: {
47
+ system: string;
48
+ code: string;
49
+ display: string;
50
+ }): FhirCodeableConcept[];
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Single source of truth mapping each deviceType to its official LOINC code,
3
+ * display text, FHIR Observation category, and standard UCUM target unit.
4
+ *
5
+ * Grounded in the official LOINC database (loinc.org) and US Core Vital Signs Profile.
6
+ */
7
+ export const VITAL_SIGNS_CATEGORY = {
8
+ system: "http://terminology.hl7.org/CodeSystem/observation-category",
9
+ code: "vital-signs",
10
+ display: "Vital Signs",
11
+ };
12
+ export const LOINC_SYSTEM = "http://loinc.org";
13
+ export const UCUM_SYSTEM = "http://unitsofmeasure.org";
14
+ export const LOINC_MAP = {
15
+ "blood-pressure": {
16
+ deviceType: "blood-pressure",
17
+ loincCode: "85354-9",
18
+ display: "Blood pressure panel with all children optional",
19
+ category: VITAL_SIGNS_CATEGORY,
20
+ defaultUcumUnit: "mm[Hg]",
21
+ defaultUnitDisplay: "mmHg",
22
+ },
23
+ "heart-rate": {
24
+ deviceType: "heart-rate",
25
+ loincCode: "8867-4",
26
+ display: "Heart rate",
27
+ category: VITAL_SIGNS_CATEGORY,
28
+ defaultUcumUnit: "/min",
29
+ defaultUnitDisplay: "beats/min",
30
+ },
31
+ weight: {
32
+ deviceType: "weight",
33
+ loincCode: "29463-7",
34
+ display: "Body weight",
35
+ category: VITAL_SIGNS_CATEGORY,
36
+ defaultUcumUnit: "kg",
37
+ defaultUnitDisplay: "kg",
38
+ },
39
+ spo2: {
40
+ deviceType: "spo2",
41
+ loincCode: "59408-5",
42
+ display: "Oxygen saturation in Arterial blood by Pulse oximetry",
43
+ category: VITAL_SIGNS_CATEGORY,
44
+ defaultUcumUnit: "%",
45
+ defaultUnitDisplay: "%",
46
+ },
47
+ temperature: {
48
+ deviceType: "temperature",
49
+ loincCode: "8310-5",
50
+ display: "Body temperature",
51
+ category: VITAL_SIGNS_CATEGORY,
52
+ defaultUcumUnit: "Cel",
53
+ defaultUnitDisplay: "°C",
54
+ },
55
+ glucose: {
56
+ deviceType: "glucose",
57
+ loincCode: "2339-0",
58
+ display: "Glucose [Mass/volume] in Blood",
59
+ category: VITAL_SIGNS_CATEGORY,
60
+ defaultUcumUnit: "mg/dL",
61
+ defaultUnitDisplay: "mg/dL",
62
+ },
63
+ "respiratory-rate": {
64
+ deviceType: "respiratory-rate",
65
+ loincCode: "9279-1",
66
+ display: "Respiratory rate",
67
+ category: VITAL_SIGNS_CATEGORY,
68
+ defaultUcumUnit: "/min",
69
+ defaultUnitDisplay: "breaths/min",
70
+ },
71
+ };
72
+ /**
73
+ * Component LOINC mappings for blood pressure components.
74
+ */
75
+ export const LOINC_BP_SYSTOLIC = {
76
+ loincCode: "8480-6",
77
+ display: "Systolic blood pressure",
78
+ ucumUnit: "mm[Hg]",
79
+ unitDisplay: "mmHg",
80
+ };
81
+ export const LOINC_BP_DIASTOLIC = {
82
+ loincCode: "8462-4",
83
+ display: "Diastolic blood pressure",
84
+ ucumUnit: "mm[Hg]",
85
+ unitDisplay: "mmHg",
86
+ };
87
+ /**
88
+ * Helper function to retrieve LOINC mapping for a deviceType.
89
+ */
90
+ export function getLoincMapping(deviceType) {
91
+ const mapping = LOINC_MAP[deviceType];
92
+ if (!mapping) {
93
+ throw new Error(`No LOINC mapping found for deviceType: ${deviceType}`);
94
+ }
95
+ return mapping;
96
+ }
97
+ /**
98
+ * Formats a Category object into a FHIR CodeableConcept array.
99
+ */
100
+ export function formatCategory(category) {
101
+ return [
102
+ {
103
+ coding: [
104
+ {
105
+ system: category.system,
106
+ code: category.code,
107
+ display: category.display,
108
+ },
109
+ ],
110
+ },
111
+ ];
112
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Device reading and FHIR Observation types for @peerbits/fhir-observation-generator.
3
+ * Grounded in US Core Vital Signs Profile and FHIR R4 Observation resource specification.
4
+ */
5
+ export type DeviceType = "blood-pressure" | "heart-rate" | "weight" | "spo2" | "temperature" | "glucose" | "respiratory-rate";
6
+ export declare const SUPPORTED_DEVICE_TYPES: DeviceType[];
7
+ export interface BloodPressureValue {
8
+ systolic: number;
9
+ diastolic: number;
10
+ }
11
+ export type DeviceReadingValue = number | BloodPressureValue;
12
+ export interface DeviceReading {
13
+ /**
14
+ * The vital type of the device reading.
15
+ */
16
+ deviceType: DeviceType;
17
+ /**
18
+ * Numeric reading value, or { systolic, diastolic } object for blood pressure.
19
+ */
20
+ value: DeviceReadingValue;
21
+ /**
22
+ * Unit of measurement as reported by the device (e.g. "degF", "lbs", "mmHg", "bpm", "%", "mg/dL").
23
+ */
24
+ unit: string;
25
+ /**
26
+ * Timestamp in ISO 8601 format (e.g. "2026-08-06T10:00:00Z").
27
+ */
28
+ timestamp: string;
29
+ /**
30
+ * Opaque reference to patient (e.g. "Patient/p-12345"). Never include real identifiers/PHI.
31
+ */
32
+ patientRef: string;
33
+ /**
34
+ * Optional opaque device identifier (e.g. "Device/d-98765").
35
+ */
36
+ deviceId?: string;
37
+ }
38
+ export interface GeneratorConfig {
39
+ /**
40
+ * Optional target unit to convert value into. If omitted, standard UCUM target unit is used.
41
+ */
42
+ targetUnit?: string;
43
+ /**
44
+ * Observation status. Defaults to "final".
45
+ */
46
+ status?: "registered" | "preliminary" | "final" | "amended" | "corrected" | "cancelled" | "entered-in-error" | "unknown";
47
+ /**
48
+ * If true, performs structural validation on the generated Observation before returning.
49
+ * Throws Error if validation fails.
50
+ */
51
+ validate?: boolean;
52
+ }
53
+ export interface FhirCoding {
54
+ system: string;
55
+ code: string;
56
+ display?: string;
57
+ }
58
+ export interface FhirCodeableConcept {
59
+ coding: FhirCoding[];
60
+ text?: string;
61
+ }
62
+ export interface FhirQuantity {
63
+ value: number;
64
+ unit: string;
65
+ system: string;
66
+ code: string;
67
+ }
68
+ export interface FhirObservationComponent {
69
+ code: FhirCodeableConcept;
70
+ valueQuantity?: FhirQuantity;
71
+ }
72
+ export interface FhirObservation {
73
+ resourceType: "Observation";
74
+ id?: string;
75
+ status: string;
76
+ category: FhirCodeableConcept[];
77
+ code: FhirCodeableConcept;
78
+ subject: {
79
+ reference: string;
80
+ };
81
+ effectiveDateTime: string;
82
+ valueQuantity?: FhirQuantity;
83
+ component?: FhirObservationComponent[];
84
+ device?: {
85
+ reference: string;
86
+ };
87
+ }
88
+ export interface FhirBundleEntry {
89
+ resource: FhirObservation;
90
+ request: {
91
+ method: "POST";
92
+ url: string;
93
+ };
94
+ }
95
+ export interface FhirBundle {
96
+ resourceType: "Bundle";
97
+ type: "transaction";
98
+ entry: FhirBundleEntry[];
99
+ }
100
+ export interface ValidationResult {
101
+ valid: boolean;
102
+ errors: string[];
103
+ }
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Device reading and FHIR Observation types for @peerbits/fhir-observation-generator.
3
+ * Grounded in US Core Vital Signs Profile and FHIR R4 Observation resource specification.
4
+ */
5
+ export const SUPPORTED_DEVICE_TYPES = [
6
+ "blood-pressure",
7
+ "heart-rate",
8
+ "weight",
9
+ "spo2",
10
+ "temperature",
11
+ "glucose",
12
+ "respiratory-rate",
13
+ ];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Standard UCUM unit conversions for @peerbits/fhir-observation-generator.
3
+ *
4
+ * All formulas use exact standard UCUM conversion factors.
5
+ * Reference unit conversions are verified against clinical standards.
6
+ */
7
+ export interface UnitConversionResult {
8
+ value: number;
9
+ unit: string;
10
+ ucumCode: string;
11
+ }
12
+ /**
13
+ * Normalizes input unit strings into standard UCUM unit codes.
14
+ */
15
+ export declare function normalizeUcumUnit(unit: string): string;
16
+ /**
17
+ * Gets human-readable display string for a UCUM unit code.
18
+ */
19
+ export declare function getUnitDisplay(ucumCode: string): string;
20
+ /**
21
+ * Converts a numeric value from source unit to target unit.
22
+ * If units are identical, returns value unchanged.
23
+ * If targetUnit is not provided, defaults to standard UCUM unit or source unit.
24
+ */
25
+ export declare function convertUnit(value: number, fromUnit: string, targetUnit?: string): UnitConversionResult;