fhir-data-utils-ts 0.3.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/skills/govern-fhir-data-utilities/SKILL.md +35 -0
- package/.codex/skills/govern-fhir-observation-components/SKILL.md +11 -0
- package/CHANGELOG.md +63 -0
- package/README.md +44 -0
- package/dist/appointment-communication-test-data.d.ts +63 -0
- package/dist/appointment-communication-test-data.js +77 -0
- package/dist/builders/department-service-graph.d.ts +32 -0
- package/dist/builders/department-service-graph.js +102 -0
- package/dist/builders/device-compliance-graph.d.ts +51 -0
- package/dist/builders/device-compliance-graph.js +115 -0
- package/dist/builders/index.d.ts +3 -0
- package/dist/builders/index.js +3 -0
- package/dist/builders/service-schedule-graph.d.ts +58 -0
- package/dist/builders/service-schedule-graph.js +149 -0
- package/dist/claims/appointment-claims.d.ts +6 -0
- package/dist/claims/appointment-claims.js +6 -0
- package/dist/claims/composition-claims.d.ts +1 -0
- package/dist/claims/composition-claims.js +1 -0
- package/dist/claims/device-metric-claims.d.ts +16 -0
- package/dist/claims/device-metric-claims.js +25 -0
- package/dist/claims/healthcare-service-claims.d.ts +32 -0
- package/dist/claims/healthcare-service-claims.js +54 -0
- package/dist/claims/index.d.ts +4 -0
- package/dist/claims/index.js +4 -0
- package/dist/claims/location-claims.d.ts +3 -0
- package/dist/claims/location-claims.js +6 -0
- package/dist/claims/organization-claims.d.ts +3 -1
- package/dist/claims/organization-claims.js +4 -1
- package/dist/claims/procedure-claims.d.ts +2 -0
- package/dist/claims/procedure-claims.js +4 -0
- package/dist/claims/regulated-authorization-claims.d.ts +17 -0
- package/dist/claims/regulated-authorization-claims.js +27 -0
- package/dist/claims/schedule-slot-claims.d.ts +45 -0
- package/dist/claims/schedule-slot-claims.js +70 -0
- package/dist/constants/fhir-resource-types.d.ts +9 -0
- package/dist/constants/fhir-resource-types.js +3 -0
- package/dist/converters/convert-appointment-communication.d.ts +14 -0
- package/dist/converters/convert-appointment-communication.js +97 -0
- package/dist/converters/convert-appointment.js +16 -7
- package/dist/converters/convert-composition.js +12 -2
- package/dist/converters/convert-device-metric.d.ts +3 -0
- package/dist/converters/convert-device-metric.js +37 -0
- package/dist/converters/convert-document-reference.js +5 -0
- package/dist/converters/convert-healthcare-service.d.ts +7 -0
- package/dist/converters/convert-healthcare-service.js +60 -0
- package/dist/converters/convert-location.d.ts +4 -0
- package/dist/converters/convert-location.js +41 -3
- package/dist/converters/convert-organization.d.ts +2 -0
- package/dist/converters/convert-organization.js +20 -3
- package/dist/converters/convert-procedure.d.ts +2 -0
- package/dist/converters/convert-procedure.js +21 -0
- package/dist/converters/convert-regulated-authorization.d.ts +3 -0
- package/dist/converters/convert-regulated-authorization.js +34 -0
- package/dist/converters/convert-schedule-slot.d.ts +9 -0
- package/dist/converters/convert-schedule-slot.js +96 -0
- package/dist/converters/convert-shared.d.ts +10 -0
- package/dist/converters/convert-shared.js +19 -0
- package/dist/converters/index.d.ts +6 -0
- package/dist/converters/index.js +6 -0
- package/dist/converters/materialize-claims-first-resource.d.ts +16 -0
- package/dist/converters/materialize-claims-first-resource.js +61 -0
- package/dist/fhir-bundle.d.ts +21 -0
- package/dist/fhir-bundle.js +74 -0
- package/dist/group-r5.d.ts +12 -0
- package/dist/group-r5.js +20 -0
- package/dist/healthcare-service-specialty-map.d.ts +32 -0
- package/dist/healthcare-service-specialty-map.js +151 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/observation-claims.d.ts +3 -0
- package/dist/observation-claims.js +3 -0
- package/dist/vital-sign-observations.d.ts +75 -1
- package/dist/vital-sign-observations.js +225 -9
- package/docs/CLINICAL_TYPED_ACCESSOR_BOUNDARY.md +44 -0
- package/docs/COMMUNICATION_BUNDLE_CONTRACT.md +61 -0
- package/docs/HEALTHCARE_SERVICE_DEPARTMENT_CATALOG.md +190 -0
- package/docs/UHC_FHIR_UTILS_MIGRATION.md +20 -1
- package/docs/VITAL_SIGN_DAILY_DOCUMENT.md +79 -0
- package/package.json +5 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate SNOMED CT C80 to LOINC specialty map supplied for review.
|
|
3
|
+
*
|
|
4
|
+
* This is not an official SNOMED or LOINC map. The declared relationship is
|
|
5
|
+
* preserved so consumers can automate only strict equivalence and present
|
|
6
|
+
* wider, related, inexact or unmatched rows for professional governance.
|
|
7
|
+
*/
|
|
8
|
+
export const ClinicalSpecialtyMappingEquivalence = Object.freeze({
|
|
9
|
+
Equivalent: 'equivalent',
|
|
10
|
+
Wider: 'wider',
|
|
11
|
+
RelatedTo: 'relatedto',
|
|
12
|
+
Inexact: 'inexact',
|
|
13
|
+
Unmatched: 'unmatched',
|
|
14
|
+
});
|
|
15
|
+
const E = ClinicalSpecialtyMappingEquivalence.Equivalent;
|
|
16
|
+
const W = ClinicalSpecialtyMappingEquivalence.Wider;
|
|
17
|
+
const R = ClinicalSpecialtyMappingEquivalence.RelatedTo;
|
|
18
|
+
const I = ClinicalSpecialtyMappingEquivalence.Inexact;
|
|
19
|
+
const U = ClinicalSpecialtyMappingEquivalence.Unmatched;
|
|
20
|
+
const suppliedRows = Object.freeze([
|
|
21
|
+
['408467006', 'Adult mental illness', 'LP172958-3', 'Mental health', W],
|
|
22
|
+
['394577000', 'Anesthetics', 'LP172918-7', 'Anesthesiology', E],
|
|
23
|
+
['394578005', 'Audiological medicine', 'LP172919-5', 'Audiology', I],
|
|
24
|
+
['421661004', 'Blood banking and transfusion medicine', 'LP172920-3', 'Blood banking and transfusion medicine', E],
|
|
25
|
+
['408462000', 'Burns care', 'LP268363-1', 'Burn management', I],
|
|
26
|
+
['394579002', 'Cardiology', 'LA7179-0', 'Medicine.cardiology', E],
|
|
27
|
+
['394804000', 'Clinical cytogenetics and molecular genetics', 'LP172929-4', 'Clinical genetics', W],
|
|
28
|
+
['394580004', 'Clinical genetics', 'LP172929-4', 'Clinical genetics', E],
|
|
29
|
+
['394803006', 'Clinical hematology', 'LP175685-9', 'Hematology', I],
|
|
30
|
+
['408480009', 'Clinical immunology', 'LP345043-6', 'Immunology', W],
|
|
31
|
+
['408454008', 'Clinical microbiology', 'LP208903-7', 'Pathology - Medical microbiology', I],
|
|
32
|
+
['394809005', 'Clinical neuro-physiology', 'LP172931-0', 'Clinical neurophysiology', E],
|
|
33
|
+
['394592004', 'Clinical oncology', 'LP172901-3', 'Oncology', I],
|
|
34
|
+
['394600006', 'Clinical pharmacology', 'LP220238-2', 'Clinical pharmacology', E],
|
|
35
|
+
['394601005', 'Clinical physiology', undefined, undefined, U],
|
|
36
|
+
['394581000', 'Community medicine', 'LP204156-6', 'Community health care', I],
|
|
37
|
+
['408478003', 'Critical care medicine', 'LP172933-6', 'Critical care medicine', E],
|
|
38
|
+
['394812008', 'Dental medicine specialties', 'LP172934-4', 'Dentistry', W],
|
|
39
|
+
['408444009', 'Dental-General dental practice', 'LP172934-4', 'Dentistry', W],
|
|
40
|
+
['394582007', 'Dermatology', 'LP172935-1', 'Dermatology', E],
|
|
41
|
+
['408475000', 'Diabetic medicine', 'LP172938-5', 'Diabetology', E],
|
|
42
|
+
['410005002', 'Dive medicine', 'LP173035-9', 'Undersea and hyperbaric medicine', I],
|
|
43
|
+
['394583002', 'Endocrinology', 'LP172941-9', 'Endocrinology', E],
|
|
44
|
+
['419772000', 'Family practice', 'LP172943-5', 'Family medicine', E],
|
|
45
|
+
['394584008', 'Gastroenterology', 'LP172945-0', 'Gastroenterology', E],
|
|
46
|
+
['408443003', 'General medical practice', 'LP173010-2', 'Primary care', I],
|
|
47
|
+
['394802001', 'General medicine', 'LP172946-8', 'General medicine', E],
|
|
48
|
+
['394915009', 'General pathology', 'LP172984-9', 'Pathology', I],
|
|
49
|
+
['394814009', 'General practice', 'LP173010-2', 'Primary care', I],
|
|
50
|
+
['394808002', 'Genito-urinary medicine', 'LP173036-7', 'Urology', W],
|
|
51
|
+
['394811001', 'Geriatric medicine', 'LP172947-6', 'Geriatric medicine', E],
|
|
52
|
+
['408446006', 'Gynecological oncology', 'LP202989-2', 'Gynecologic oncology', E],
|
|
53
|
+
['394586005', 'Gynecology', 'LP221284-5', 'Gynecology', E],
|
|
54
|
+
['394916005', 'Hematopathology', 'LP172984-9', 'Pathology', W],
|
|
55
|
+
['408472002', 'Hepatology', 'LP172950-0', 'Hepatology', E],
|
|
56
|
+
['394597005', 'Histopathology', 'LP208907-8', 'Anatomic pathology', W],
|
|
57
|
+
['394598000', 'Immunopathology', 'LP345043-6', 'Immunology', W],
|
|
58
|
+
['394807007', 'Infectious diseases', 'LP172951-8', 'Infectious disease', E],
|
|
59
|
+
['419192003', 'Internal medicine', 'LP172952-6', 'Internal medicine', E],
|
|
60
|
+
['408468001', 'Learning disability', undefined, undefined, U],
|
|
61
|
+
['394593009', 'Medical oncology', 'LP175686-7', 'Medical Oncology', E],
|
|
62
|
+
['394813003', 'Medical ophthalmology', 'LP172974-0', 'Ophthalmology', W],
|
|
63
|
+
['410001006', 'Military medicine', undefined, undefined, U],
|
|
64
|
+
['394589003', 'Nephrology', 'LP172962-5', 'Nephrology', E],
|
|
65
|
+
['394591006', 'Neurology', 'LP172964-1', 'Neurology', E],
|
|
66
|
+
['394599008', 'Neuropathology', 'LP208907-8', 'Anatomic pathology', W],
|
|
67
|
+
['394649004', 'Nuclear medicine', 'LP172968-2', 'Nuclear medicine', E],
|
|
68
|
+
['408470005', 'Obstetrics', 'LP221283-7', 'Obstetrics', E],
|
|
69
|
+
['394585009', 'Obstetrics and gynecology', 'LP172971-6', 'Obstetrics and gynecology', E],
|
|
70
|
+
['394821009', 'Occupational medicine', 'LP172972-4', 'Occupational medicine', E],
|
|
71
|
+
['422191005', 'Ophthalmic surgery', 'LP172974-0', 'Ophthalmology', W],
|
|
72
|
+
['394594003', 'Ophthalmology', 'LP172974-0', 'Ophthalmology', E],
|
|
73
|
+
['416304004', 'Osteopathic manipulative medicine', 'LP434767-2', 'Osteopathic medicine', I],
|
|
74
|
+
['418960008', 'Otolaryngology', 'LP172980-7', 'Otolaryngology', E],
|
|
75
|
+
['394882004', 'Pain management', 'LP172981-5', 'Pain medicine', I],
|
|
76
|
+
['394806003', 'Palliative medicine', 'LP172982-3', 'Palliative care', I],
|
|
77
|
+
['394588006', 'Pediatric (Child and adolescent) psychiatry', 'LP172922-9', 'Child and adolescent psychiatry', E],
|
|
78
|
+
['408459003', 'Pediatric cardiology', 'LP172985-6', 'Pediatric cardiology', E],
|
|
79
|
+
['394607009', 'Pediatric dentistry', 'LP172934-4', 'Dentistry', W],
|
|
80
|
+
['419610006', 'Pediatric endocrinology', 'LP172989-8', 'Pediatric endocrinology', E],
|
|
81
|
+
['418058008', 'Pediatric gastroenterology', 'LP172990-6', 'Pediatric gastroenterology', E],
|
|
82
|
+
['420208008', 'Pediatric genetics', 'LP172956-7', 'Medical genetics', W],
|
|
83
|
+
['418652005', 'Pediatric hematology', 'LP172991-4', 'Pediatric hematology-oncology', W],
|
|
84
|
+
['418535003', 'Pediatric immunology', 'LP345043-6', 'Immunology', W],
|
|
85
|
+
['418862001', 'Pediatric infectious diseases', 'LP172992-2', 'Pediatric infectious diseases', E],
|
|
86
|
+
['419365004', 'Pediatric nephrology', 'LP172993-0', 'Pediatric nephrology', E],
|
|
87
|
+
['418002000', 'Pediatric oncology', 'LP172991-4', 'Pediatric hematology-oncology', I],
|
|
88
|
+
['419983000', 'Pediatric ophthalmology', 'LP172974-0', 'Ophthalmology', W],
|
|
89
|
+
['419170002', 'Pediatric pulmonology', 'LP172995-5', 'Pediatric pulmonology', E],
|
|
90
|
+
['419472004', 'Pediatric rheumatology', 'LP172998-9', 'Pediatric rheumatology', E],
|
|
91
|
+
['394539006', 'Pediatric surgery', 'LP172999-7', 'Pediatric surgery', E],
|
|
92
|
+
['420112009', 'Pediatric surgery-bone marrow transplantation', 'LP417851-5', 'Bone Marrow Transplant', I],
|
|
93
|
+
['409968004', 'Preventive medicine', 'LP173009-4', 'Preventive medicine', E],
|
|
94
|
+
['394587001', 'Psychiatry', 'LP173011-0', 'Psychiatry', E],
|
|
95
|
+
['394913002', 'Psychotherapy', 'LP173012-8', 'Psychology', R],
|
|
96
|
+
['408440000', 'Public health medicine', 'LP173014-4', 'Public health', I],
|
|
97
|
+
['418112009', 'Pulmonary medicine', 'LP173015-1', 'Pulmonary disease', I],
|
|
98
|
+
['419815003', 'Radiation oncology', 'LP173016-9', 'Radiation oncology', E],
|
|
99
|
+
['394914008', 'Radiology', 'LP173018-5', 'Radiology', E],
|
|
100
|
+
['408455009', 'Radiology-Interventional radiology', 'LP172896-5', 'Interventional radiology', E],
|
|
101
|
+
['394602003', 'Rehabilitation', 'LP173004-5', 'Physical medicine and rehabilitation', I],
|
|
102
|
+
['408447002', 'Respite care', 'LP173240-5', 'Respite', R],
|
|
103
|
+
['394810000', 'Rheumatology', 'LP173023-5', 'Rheumatology', E],
|
|
104
|
+
['408450004', 'Sleep studies', 'LP248728-0', 'Sleep medicine', I],
|
|
105
|
+
['408476004', 'Surgery-Bone and marrow transplantation', 'LP417851-5', 'Bone Marrow Transplant', I],
|
|
106
|
+
['408469009', 'Surgery-Breast surgery', 'LA7201-2', 'Surgery.breast', E],
|
|
107
|
+
['408466002', 'Surgery-Cardiac surgery', 'LP207300-7', 'Cardiac surgery', E],
|
|
108
|
+
['408471009', 'Surgery-Cardiothoracic transplantation', 'LP173033-4', 'Transplant surgery', W],
|
|
109
|
+
['408464004', 'Surgery-Colorectal surgery', 'LP172932-8', 'Colon and rectal surgery', E],
|
|
110
|
+
['408441001', 'Surgery-Dental-Endodontics', 'LP172934-4', 'Dentistry', W],
|
|
111
|
+
['408465003', 'Surgery-Dental-Oral and maxillofacial surgery', 'LP172902-1', 'Oral and maxillofacial surgery', E],
|
|
112
|
+
['394605001', 'Surgery-Dental-Oral surgery', 'LP172902-1', 'Oral and maxillofacial surgery', I],
|
|
113
|
+
['394608004', 'Surgery-Dental-Orthodontics / Dentistry-surgical-Orthodontics', 'LP172934-4', 'Dentistry', W],
|
|
114
|
+
['408461007', 'Surgery-Dental-Periodontal surgery', 'LP172934-4', 'Dentistry', W],
|
|
115
|
+
['408460008', 'Prosthodontics / surgical-Prosthodontics', 'LP172934-4', 'Dentistry', W],
|
|
116
|
+
['394606000', 'Surgery-Dentistry-Restorative dentistry', 'LP172934-4', 'Dentistry', W],
|
|
117
|
+
['408449004', 'Surgery-Dentistry--surgical', 'LP172934-4', 'Dentistry', W],
|
|
118
|
+
['418018006', 'Surgery-Dermatologic surgery', 'LP172935-1', 'Dermatology', W],
|
|
119
|
+
['394604002', 'Surgery-Ear, nose and throat surgery', 'LP172980-7', 'Otolaryngology', I],
|
|
120
|
+
['394609007', 'Surgery-general', 'LA7205-3', 'Surgery.general', E],
|
|
121
|
+
['408474001', 'Surgery-Hepatobiliary and pancreatic surgery', 'LP172950-0', 'Hepatology', W],
|
|
122
|
+
['394610002', 'Surgery-Neurosurgery', 'LP172963-3', 'Neurological surgery', E],
|
|
123
|
+
['394611003', 'Surgery-Plastic surgery', 'LP173006-0', 'Plastic surgery', E],
|
|
124
|
+
['408477008', 'Surgery-Transplantation surgery', 'LP173033-4', 'Transplant surgery', E],
|
|
125
|
+
['394801008', 'Surgery-Trauma and orthopedics', 'LP434806-8', 'Orthopaedic surgery', I],
|
|
126
|
+
['408463005', 'Surgery-Vascular', 'LP173039-1', 'Vascular surgery', E],
|
|
127
|
+
['419321007', 'Surgical oncology', 'LP207940-0', 'Surgical oncology', E],
|
|
128
|
+
['394576009', 'Surgical-Accident & emergency', 'LP172940-1', 'Emergency medicine', I],
|
|
129
|
+
['394590007', 'Thoracic medicine', 'LP173031-8', 'Thoracic and cardiac surgery', R],
|
|
130
|
+
['409967009', 'Toxicology', 'LP172957-5', 'Medical toxicology', I],
|
|
131
|
+
['408448007', 'Tropical medicine', undefined, undefined, U],
|
|
132
|
+
['419043006', 'Urological oncology', 'LP173036-7', 'Urology', W],
|
|
133
|
+
['394612005', 'Urology', 'LP173036-7', 'Urology', E],
|
|
134
|
+
['394733009', 'Medical specialty--OTHER--NOT LISTED', 'LA46-8', 'Other', I],
|
|
135
|
+
['394732004', 'Surgical specialty--OTHER-NOT LISTED', 'LA46-8', 'Other', I],
|
|
136
|
+
]);
|
|
137
|
+
export const SnomedC80ToLoincSpecialtyConceptMap = Object.freeze(suppliedRows.map(([snomedCode, snomedDisplay, loincCode, loincDisplay, equivalence = U]) => Object.freeze({
|
|
138
|
+
snomedSystem: 'http://snomed.info/sct',
|
|
139
|
+
snomedCode,
|
|
140
|
+
snomedDisplay,
|
|
141
|
+
...(loincCode ? { loincSystem: 'http://loinc.org', loincCode, loincDisplay } : {}),
|
|
142
|
+
equivalence,
|
|
143
|
+
automatic: equivalence === E,
|
|
144
|
+
})));
|
|
145
|
+
/** Returns a portable LOINC specialty only for a strict reviewed equivalence. */
|
|
146
|
+
export function findAutomaticLoincSpecialty(snomedCode) {
|
|
147
|
+
const mapping = SnomedC80ToLoincSpecialtyConceptMap.find(item => item.snomedCode === snomedCode);
|
|
148
|
+
if (!mapping?.automatic || !mapping.loincCode || !mapping.loincDisplay)
|
|
149
|
+
return undefined;
|
|
150
|
+
return Object.freeze({ system: 'http://loinc.org', code: mapping.loincCode, display: mapping.loincDisplay });
|
|
151
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,3 +24,5 @@ export * from './fhir-tokens.js';
|
|
|
24
24
|
export * from './related-person.js';
|
|
25
25
|
export * from './imaging-types.js';
|
|
26
26
|
export * from './subscription-r5.js';
|
|
27
|
+
export * from './healthcare-service-specialty-map.js';
|
|
28
|
+
export * from './appointment-communication-test-data.js';
|
package/dist/index.js
CHANGED
|
@@ -24,3 +24,5 @@ export * from './fhir-tokens.js';
|
|
|
24
24
|
export * from './related-person.js';
|
|
25
25
|
export * from './imaging-types.js';
|
|
26
26
|
export * from './subscription-r5.js';
|
|
27
|
+
export * from './healthcare-service-specialty-map.js';
|
|
28
|
+
export * from './appointment-communication-test-data.js';
|
|
@@ -85,6 +85,7 @@ export declare const ObservationClaim: Readonly<{
|
|
|
85
85
|
readonly Code: "Observation.code";
|
|
86
86
|
readonly Date: "Observation.date";
|
|
87
87
|
readonly Device: "Observation.device";
|
|
88
|
+
readonly DerivedFrom: "Observation.derived-from";
|
|
88
89
|
readonly Encounter: "Observation.encounter";
|
|
89
90
|
readonly Focus: "Observation.focus";
|
|
90
91
|
readonly HasMember: "Observation.has-member";
|
|
@@ -122,6 +123,8 @@ export declare const ObservationClaim: Readonly<{
|
|
|
122
123
|
readonly ValueString: "Observation.value-string";
|
|
123
124
|
readonly Note: "Observation.note";
|
|
124
125
|
readonly EffectiveDateTime: "Observation.effective-datetime";
|
|
126
|
+
/** FHIR EventTiming bucket used by recurring morning/evening capture. */
|
|
127
|
+
readonly DateWhen: "Observation.date-when";
|
|
125
128
|
/** @deprecated Read-only camelCase key from historical persisted rows. */
|
|
126
129
|
readonly EffectiveDateTimeLegacy: "Observation.effectiveDateTime";
|
|
127
130
|
/** Custom provenance flag for an explicitly entered or selected result. */
|
|
@@ -36,6 +36,7 @@ export const ObservationClaim = Object.freeze({
|
|
|
36
36
|
Code: 'Observation.code',
|
|
37
37
|
Date: 'Observation.date',
|
|
38
38
|
Device: 'Observation.device',
|
|
39
|
+
DerivedFrom: 'Observation.derived-from',
|
|
39
40
|
Encounter: 'Observation.encounter',
|
|
40
41
|
Focus: 'Observation.focus',
|
|
41
42
|
HasMember: 'Observation.has-member',
|
|
@@ -73,6 +74,8 @@ export const ObservationClaim = Object.freeze({
|
|
|
73
74
|
ValueString: 'Observation.value-string',
|
|
74
75
|
Note: 'Observation.note',
|
|
75
76
|
EffectiveDateTime: 'Observation.effective-datetime',
|
|
77
|
+
/** FHIR EventTiming bucket used by recurring morning/evening capture. */
|
|
78
|
+
DateWhen: 'Observation.date-when',
|
|
76
79
|
/** @deprecated Read-only camelCase key from historical persisted rows. */
|
|
77
80
|
EffectiveDateTimeLegacy: 'Observation.effectiveDateTime',
|
|
78
81
|
/** Custom provenance flag for an explicitly entered or selected result. */
|
|
@@ -71,6 +71,12 @@ export declare const VitalSignsUnits: Readonly<{
|
|
|
71
71
|
display?: string;
|
|
72
72
|
claim: string;
|
|
73
73
|
}>;
|
|
74
|
+
readonly Fahrenheit: Readonly<{
|
|
75
|
+
system: string;
|
|
76
|
+
code: string;
|
|
77
|
+
display?: string;
|
|
78
|
+
claim: string;
|
|
79
|
+
}>;
|
|
74
80
|
readonly Percent: Readonly<{
|
|
75
81
|
system: string;
|
|
76
82
|
code: string;
|
|
@@ -83,6 +89,12 @@ export declare const VitalSignsUnits: Readonly<{
|
|
|
83
89
|
display?: string;
|
|
84
90
|
claim: string;
|
|
85
91
|
}>;
|
|
92
|
+
readonly Pound: Readonly<{
|
|
93
|
+
system: string;
|
|
94
|
+
code: string;
|
|
95
|
+
display?: string;
|
|
96
|
+
claim: string;
|
|
97
|
+
}>;
|
|
86
98
|
}>;
|
|
87
99
|
/** Governed FHIR/LOINC tokens used by every channel that captures vital signs. */
|
|
88
100
|
export declare const VitalSignCode: Readonly<{
|
|
@@ -99,19 +111,24 @@ export declare const VitalSignCode: Readonly<{
|
|
|
99
111
|
export declare const VitalSignUnit: Readonly<{
|
|
100
112
|
readonly bloodPressure: string;
|
|
101
113
|
readonly temperature: string;
|
|
114
|
+
readonly temperatureFahrenheit: string;
|
|
102
115
|
readonly heartRate: string;
|
|
103
116
|
readonly oxygenSaturation: string;
|
|
104
117
|
readonly respiratoryRate: string;
|
|
105
118
|
readonly bodyWeight: string;
|
|
119
|
+
readonly bodyWeightPound: string;
|
|
106
120
|
}>;
|
|
121
|
+
export type VitalSignQuantityUnit = typeof VitalSignUnit[keyof typeof VitalSignUnit];
|
|
107
122
|
export type VitalSignKind = 'blood-pressure' | 'temperature' | 'heart-rate' | 'oxygen-saturation' | 'respiratory-rate' | 'body-weight';
|
|
108
|
-
export type VitalSignObservationStatus = 'preliminary' | 'final';
|
|
123
|
+
export type VitalSignObservationStatus = 'registered' | 'preliminary' | 'final';
|
|
124
|
+
export type VitalSignEventTiming = 'MORN' | 'AFT' | 'EVE' | 'NIGHT';
|
|
109
125
|
type CommonMeasurement = Readonly<{
|
|
110
126
|
entryId: string;
|
|
111
127
|
subjectReference: string;
|
|
112
128
|
kind: VitalSignKind;
|
|
113
129
|
measuredAt: string;
|
|
114
130
|
status: VitalSignObservationStatus;
|
|
131
|
+
eventTiming?: VitalSignEventTiming;
|
|
115
132
|
deviceReference?: string;
|
|
116
133
|
note?: string;
|
|
117
134
|
userSelected?: boolean;
|
|
@@ -123,6 +140,7 @@ export type VitalSignMeasurement = CommonMeasurement & (Readonly<{
|
|
|
123
140
|
}> | Readonly<{
|
|
124
141
|
kind: Exclude<VitalSignKind, 'blood-pressure'>;
|
|
125
142
|
value: number;
|
|
143
|
+
unit?: VitalSignQuantityUnit;
|
|
126
144
|
}>);
|
|
127
145
|
/**
|
|
128
146
|
* Canonical claims-first representation of one native FHIR Observation.
|
|
@@ -218,6 +236,7 @@ export type VitalSignDeviceReading = Readonly<{
|
|
|
218
236
|
kind: Exclude<VitalSignKind, 'blood-pressure'>;
|
|
219
237
|
measuredAt: string;
|
|
220
238
|
value: number;
|
|
239
|
+
unit?: VitalSignQuantityUnit;
|
|
221
240
|
note?: string;
|
|
222
241
|
}>;
|
|
223
242
|
/** Converts a device/API batch to independent preliminary Observations without aggregation. */
|
|
@@ -226,6 +245,61 @@ export declare function normalizeVitalSignDeviceBatch(input: Readonly<{
|
|
|
226
245
|
deviceReference: string;
|
|
227
246
|
readings: readonly VitalSignDeviceReading[];
|
|
228
247
|
}>): readonly VitalSignObservationGraph[];
|
|
248
|
+
export type RepeatedVitalSignReading = Readonly<{
|
|
249
|
+
entryId: string;
|
|
250
|
+
measuredAt: string;
|
|
251
|
+
systolic: number;
|
|
252
|
+
diastolic: number;
|
|
253
|
+
note?: string;
|
|
254
|
+
}> | Readonly<{
|
|
255
|
+
entryId: string;
|
|
256
|
+
measuredAt: string;
|
|
257
|
+
value: number;
|
|
258
|
+
unit?: VitalSignQuantityUnit;
|
|
259
|
+
note?: string;
|
|
260
|
+
}>;
|
|
261
|
+
export type RepeatedVitalSignSession = Readonly<{
|
|
262
|
+
eventTiming: VitalSignEventTiming;
|
|
263
|
+
sourceGraphs: readonly VitalSignObservationGraph[];
|
|
264
|
+
averageGraph: VitalSignObservationGraph;
|
|
265
|
+
entries: readonly FlatClaimResourceEntry[];
|
|
266
|
+
}>;
|
|
267
|
+
/**
|
|
268
|
+
* Converts exactly three readings taken two minutes apart into registered raw
|
|
269
|
+
* evidence plus one preliminary arithmetic mean. Confirmation belongs to the
|
|
270
|
+
* daily document boundary; this helper never invents an attester.
|
|
271
|
+
*/
|
|
272
|
+
export declare function buildRepeatedVitalSignAverage(input: Readonly<{
|
|
273
|
+
entryId: string;
|
|
274
|
+
subjectReference: string;
|
|
275
|
+
deviceReference?: string;
|
|
276
|
+
kind: VitalSignKind;
|
|
277
|
+
eventTiming: VitalSignEventTiming;
|
|
278
|
+
readings: readonly RepeatedVitalSignReading[];
|
|
279
|
+
}>): RepeatedVitalSignSession;
|
|
280
|
+
export type VitalSignDailyDocument = Readonly<{
|
|
281
|
+
localDate: string;
|
|
282
|
+
timeZone: string;
|
|
283
|
+
status: 'preliminary' | 'final';
|
|
284
|
+
composition: FlatClaimResourceEntry;
|
|
285
|
+
observations: readonly VitalSignObservationGraph[];
|
|
286
|
+
entries: readonly FlatClaimResourceEntry[];
|
|
287
|
+
}>;
|
|
288
|
+
/** Builds one daily FHIR document graph; hashing, indexing and research remain service policy. */
|
|
289
|
+
export declare function buildVitalSignDailyDocument(input: Readonly<{
|
|
290
|
+
entryId: string;
|
|
291
|
+
identifier: string;
|
|
292
|
+
subjectReference: string;
|
|
293
|
+
authorReference: string;
|
|
294
|
+
localDate: string;
|
|
295
|
+
timeZone: string;
|
|
296
|
+
observationGraphs: readonly VitalSignObservationGraph[];
|
|
297
|
+
}>): VitalSignDailyDocument;
|
|
298
|
+
/** Confirms a preliminary daily document and finalizes its top-level Observations atomically in memory. */
|
|
299
|
+
export declare function finalizeVitalSignDailyDocument(draft: VitalSignDailyDocument, confirmation: Readonly<{
|
|
300
|
+
attesterReference: string;
|
|
301
|
+
confirmedAt: string;
|
|
302
|
+
}>): VitalSignDailyDocument;
|
|
229
303
|
/** Reusable neutral fixtures; consumers and tests must not duplicate wire examples. */
|
|
230
304
|
export declare const VITAL_SIGN_EXAMPLES: Readonly<{
|
|
231
305
|
subjectReference: "Patient/example-subject";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildFlatClaimResourceEntry } from './flat-claim-resource-graph.js';
|
|
2
|
+
import { CompositionAttesterModes, CompositionClaim } from './claims/composition-claims.js';
|
|
2
3
|
import { FhirCodeSystem, ObservationCategoryCodes, ObservationClaim, decodeObservationClaimList, } from './observation-claims.js';
|
|
3
4
|
export { ObservationCategoryCodes, ObservationClaim } from './observation-claims.js';
|
|
4
5
|
function defineCoding(system, code, display) {
|
|
@@ -20,8 +21,10 @@ export const VitalSignsUnits = Object.freeze({
|
|
|
20
21
|
BeatsPerMinute: defineCoding(FhirCodeSystem.Ucum, '/min', 'beats/minute'),
|
|
21
22
|
MillimeterOfMercury: defineCoding(FhirCodeSystem.Ucum, 'mm[Hg]', 'mmHg'),
|
|
22
23
|
Celsius: defineCoding(FhirCodeSystem.Ucum, 'Cel', 'degrees Celsius'),
|
|
24
|
+
Fahrenheit: defineCoding(FhirCodeSystem.Ucum, '[degF]', 'degrees Fahrenheit'),
|
|
23
25
|
Percent: defineCoding(FhirCodeSystem.Ucum, '%', 'percent'),
|
|
24
26
|
Kilogram: defineCoding(FhirCodeSystem.Ucum, 'kg', 'kilogram'),
|
|
27
|
+
Pound: defineCoding(FhirCodeSystem.Ucum, '[lb_av]', 'pound'),
|
|
25
28
|
});
|
|
26
29
|
/** Governed FHIR/LOINC tokens used by every channel that captures vital signs. */
|
|
27
30
|
export const VitalSignCode = Object.freeze({
|
|
@@ -38,10 +41,12 @@ export const VitalSignCode = Object.freeze({
|
|
|
38
41
|
export const VitalSignUnit = Object.freeze({
|
|
39
42
|
bloodPressure: VitalSignsUnits.MillimeterOfMercury.claim,
|
|
40
43
|
temperature: VitalSignsUnits.Celsius.claim,
|
|
44
|
+
temperatureFahrenheit: VitalSignsUnits.Fahrenheit.claim,
|
|
41
45
|
heartRate: VitalSignsUnits.BeatsPerMinute.claim,
|
|
42
46
|
oxygenSaturation: VitalSignsUnits.Percent.claim,
|
|
43
47
|
respiratoryRate: VitalSignsUnits.BeatsPerMinute.claim,
|
|
44
48
|
bodyWeight: VitalSignsUnits.Kilogram.claim,
|
|
49
|
+
bodyWeightPound: VitalSignsUnits.Pound.claim,
|
|
45
50
|
});
|
|
46
51
|
/**
|
|
47
52
|
* Builds one timestamped Observation. The device is retained as source; this
|
|
@@ -58,6 +63,8 @@ export function buildVitalSignObservation(input) {
|
|
|
58
63
|
};
|
|
59
64
|
if (input.deviceReference)
|
|
60
65
|
claims[ObservationClaim.Device] = required(input.deviceReference, 'vital_sign_device_required');
|
|
66
|
+
if (input.eventTiming)
|
|
67
|
+
claims[ObservationClaim.DateWhen] = normalizeEventTiming(input.eventTiming);
|
|
61
68
|
if (input.note?.trim())
|
|
62
69
|
claims[ObservationClaim.Note] = input.note.trim();
|
|
63
70
|
if (input.userSelected !== undefined)
|
|
@@ -79,7 +86,7 @@ export function buildVitalSignObservation(input) {
|
|
|
79
86
|
else {
|
|
80
87
|
if (!validPositive(input.value))
|
|
81
88
|
throw new TypeError('vital_sign_value_invalid');
|
|
82
|
-
const scalar = scalarDefinition(input.kind);
|
|
89
|
+
const scalar = scalarDefinition(input.kind, input.unit);
|
|
83
90
|
claims[ObservationClaim.Code] = scalar.coding.claim;
|
|
84
91
|
if (scalar.coding.display)
|
|
85
92
|
claims[ObservationClaim.CodeDisplay] = scalar.coding.display;
|
|
@@ -213,6 +220,123 @@ export function normalizeVitalSignDeviceBatch(input) {
|
|
|
213
220
|
});
|
|
214
221
|
}));
|
|
215
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Converts exactly three readings taken two minutes apart into registered raw
|
|
225
|
+
* evidence plus one preliminary arithmetic mean. Confirmation belongs to the
|
|
226
|
+
* daily document boundary; this helper never invents an attester.
|
|
227
|
+
*/
|
|
228
|
+
export function buildRepeatedVitalSignAverage(input) {
|
|
229
|
+
if (input.readings.length !== 3)
|
|
230
|
+
throw new TypeError('vital_sign_repeated_reading_count_invalid');
|
|
231
|
+
const instants = input.readings.map(reading => normalizeDateTime(reading.measuredAt));
|
|
232
|
+
for (let index = 1; index < instants.length; index += 1) {
|
|
233
|
+
if (Date.parse(instants[index]) - Date.parse(instants[index - 1]) !== 120_000) {
|
|
234
|
+
throw new TypeError('vital_sign_repeated_reading_cadence_invalid');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const eventTiming = normalizeEventTiming(input.eventTiming);
|
|
238
|
+
const sourceGraphs = Object.freeze(input.readings.map(reading => buildVitalSignObservation({
|
|
239
|
+
...reading,
|
|
240
|
+
subjectReference: input.subjectReference,
|
|
241
|
+
...(input.deviceReference ? { deviceReference: input.deviceReference } : {}),
|
|
242
|
+
kind: input.kind,
|
|
243
|
+
status: 'registered',
|
|
244
|
+
eventTiming,
|
|
245
|
+
})));
|
|
246
|
+
const common = {
|
|
247
|
+
entryId: input.entryId,
|
|
248
|
+
subjectReference: input.subjectReference,
|
|
249
|
+
...(input.deviceReference ? { deviceReference: input.deviceReference } : {}),
|
|
250
|
+
kind: input.kind,
|
|
251
|
+
measuredAt: instants[1],
|
|
252
|
+
status: 'preliminary',
|
|
253
|
+
eventTiming,
|
|
254
|
+
...(input.kind === 'blood-pressure' ? {} : { unit: repeatedScalarUnit(input.kind, input.readings) }),
|
|
255
|
+
};
|
|
256
|
+
const average = input.kind === 'blood-pressure'
|
|
257
|
+
? buildVitalSignObservation({
|
|
258
|
+
...common,
|
|
259
|
+
kind: 'blood-pressure',
|
|
260
|
+
systolic: mean(input.readings.map(reading => bloodPressureValue(reading, 'systolic'))),
|
|
261
|
+
diastolic: mean(input.readings.map(reading => bloodPressureValue(reading, 'diastolic'))),
|
|
262
|
+
})
|
|
263
|
+
: buildVitalSignObservation({
|
|
264
|
+
...common,
|
|
265
|
+
kind: input.kind,
|
|
266
|
+
value: mean(input.readings.map(scalarReadingValue)),
|
|
267
|
+
});
|
|
268
|
+
const derivedFrom = sourceGraphs.map(graph => graph.primary.reference).join(',');
|
|
269
|
+
const averageGraph = replacePrimaryClaims(average, {
|
|
270
|
+
...average.primary.claims,
|
|
271
|
+
[ObservationClaim.DerivedFrom]: derivedFrom,
|
|
272
|
+
});
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
eventTiming,
|
|
275
|
+
sourceGraphs,
|
|
276
|
+
averageGraph,
|
|
277
|
+
entries: Object.freeze([...sourceGraphs.flatMap(graph => graph.entries), ...averageGraph.entries]),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** Builds one daily FHIR document graph; hashing, indexing and research remain service policy. */
|
|
281
|
+
export function buildVitalSignDailyDocument(input) {
|
|
282
|
+
const localDate = normalizeLocalDate(input.localDate);
|
|
283
|
+
const timeZone = normalizeTimeZone(input.timeZone);
|
|
284
|
+
if (!input.observationGraphs.length)
|
|
285
|
+
throw new TypeError('vital_sign_daily_observations_required');
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
for (const graph of input.observationGraphs) {
|
|
288
|
+
if (graph.primary.claims[ObservationClaim.Subject] !== input.subjectReference)
|
|
289
|
+
throw new TypeError('vital_sign_daily_subject_mismatch');
|
|
290
|
+
const measuredAt = required(graph.primary.claims[ObservationClaim.EffectiveDateTime] ?? '', 'vital_sign_measured_at_required');
|
|
291
|
+
if (localDateAt(measuredAt, timeZone) !== localDate)
|
|
292
|
+
throw new TypeError('vital_sign_daily_local_date_mismatch');
|
|
293
|
+
for (const entry of graph.entries) {
|
|
294
|
+
if (seen.has(entry.reference))
|
|
295
|
+
throw new TypeError('vital_sign_daily_entry_duplicate');
|
|
296
|
+
seen.add(entry.reference);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const observationReferences = input.observationGraphs.map(graph => graph.primary.reference);
|
|
300
|
+
const composition = buildFlatClaimResourceEntry({
|
|
301
|
+
entryId: input.entryId,
|
|
302
|
+
resourceType: 'Composition',
|
|
303
|
+
claims: {
|
|
304
|
+
[CompositionClaim.Identifier]: required(input.identifier, 'vital_sign_daily_identifier_required'),
|
|
305
|
+
[CompositionClaim.Status]: 'preliminary',
|
|
306
|
+
[CompositionClaim.Subject]: required(input.subjectReference, 'vital_sign_subject_required'),
|
|
307
|
+
[CompositionClaim.Author]: required(input.authorReference, 'vital_sign_daily_author_required'),
|
|
308
|
+
[CompositionClaim.Date]: latestEffectiveDateTime(input.observationGraphs),
|
|
309
|
+
[CompositionClaim.Type]: `${FhirCodeSystem.Loinc}|8716-3`,
|
|
310
|
+
[CompositionClaim.Title]: `Daily vital signs ${localDate}`,
|
|
311
|
+
[CompositionClaim.Entry]: observationReferences.join(','),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
return freezeDailyDocument(localDate, timeZone, 'preliminary', composition, input.observationGraphs);
|
|
315
|
+
}
|
|
316
|
+
/** Confirms a preliminary daily document and finalizes its top-level Observations atomically in memory. */
|
|
317
|
+
export function finalizeVitalSignDailyDocument(draft, confirmation) {
|
|
318
|
+
if (draft.status !== 'preliminary' || draft.composition.claims[CompositionClaim.Status] !== 'preliminary') {
|
|
319
|
+
throw new TypeError('vital_sign_daily_preliminary_required');
|
|
320
|
+
}
|
|
321
|
+
const confirmedAt = normalizeDateTime(confirmation.confirmedAt);
|
|
322
|
+
const attesterReference = required(confirmation.attesterReference, 'vital_sign_daily_attester_required');
|
|
323
|
+
const observations = Object.freeze(draft.observations.map(graph => replacePrimaryClaims(graph, {
|
|
324
|
+
...graph.primary.claims,
|
|
325
|
+
[ObservationClaim.Status]: 'final',
|
|
326
|
+
})));
|
|
327
|
+
const composition = buildFlatClaimResourceEntry({
|
|
328
|
+
entryId: draft.composition.entryId,
|
|
329
|
+
resourceType: 'Composition',
|
|
330
|
+
claims: {
|
|
331
|
+
...draft.composition.claims,
|
|
332
|
+
[CompositionClaim.Status]: 'final',
|
|
333
|
+
[CompositionClaim.Attester]: attesterReference,
|
|
334
|
+
[CompositionClaim.AttesterMode]: CompositionAttesterModes.Professional,
|
|
335
|
+
[CompositionClaim.AttesterTime]: confirmedAt,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return freezeDailyDocument(draft.localDate, draft.timeZone, 'final', composition, observations);
|
|
339
|
+
}
|
|
216
340
|
function buildBloodPressureComponent(input, role, value, coding) {
|
|
217
341
|
const entryId = `${required(input.entryId, 'vital_sign_entry_id_required')}-${role}`;
|
|
218
342
|
const claims = {
|
|
@@ -228,6 +352,8 @@ function buildBloodPressureComponent(input, role, value, coding) {
|
|
|
228
352
|
claims[ObservationClaim.CodeDisplay] = coding.display;
|
|
229
353
|
if (input.deviceReference)
|
|
230
354
|
claims[ObservationClaim.Device] = required(input.deviceReference, 'vital_sign_device_required');
|
|
355
|
+
if (input.eventTiming)
|
|
356
|
+
claims[ObservationClaim.DateWhen] = normalizeEventTiming(input.eventTiming);
|
|
231
357
|
return buildFlatClaimResourceEntry({ entryId, resourceType: 'Observation', claims });
|
|
232
358
|
}
|
|
233
359
|
function freezeObservationGraph(primary, components) {
|
|
@@ -248,14 +374,20 @@ function componentQuantityFromEntry(entry) {
|
|
|
248
374
|
const display = entry.claims[ObservationClaim.CodeDisplay];
|
|
249
375
|
return Object.freeze({ code, ...(display ? { display } : {}), value, unit });
|
|
250
376
|
}
|
|
251
|
-
function scalarDefinition(kind) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
377
|
+
function scalarDefinition(kind, requestedUnit) {
|
|
378
|
+
const definition = (() => {
|
|
379
|
+
switch (kind) {
|
|
380
|
+
case 'temperature': return { coding: VitalSignsCodes.BodyTemperature, units: [VitalSignUnit.temperature, VitalSignUnit.temperatureFahrenheit] };
|
|
381
|
+
case 'heart-rate': return { coding: VitalSignsCodes.HeartRate, units: [VitalSignUnit.heartRate] };
|
|
382
|
+
case 'oxygen-saturation': return { coding: VitalSignsCodes.OxygenSaturation, units: [VitalSignUnit.oxygenSaturation] };
|
|
383
|
+
case 'respiratory-rate': return { coding: VitalSignsCodes.RespiratoryRate, units: [VitalSignUnit.respiratoryRate] };
|
|
384
|
+
case 'body-weight': return { coding: VitalSignsCodes.BodyWeight, units: [VitalSignUnit.bodyWeight, VitalSignUnit.bodyWeightPound] };
|
|
385
|
+
}
|
|
386
|
+
})();
|
|
387
|
+
const unit = requestedUnit ?? definition.units[0];
|
|
388
|
+
if (!definition.units.includes(unit))
|
|
389
|
+
throw new TypeError('vital_sign_unit_invalid');
|
|
390
|
+
return { coding: definition.coding, unit };
|
|
259
391
|
}
|
|
260
392
|
function normalizeDateTime(value) {
|
|
261
393
|
const date = new Date(value);
|
|
@@ -263,6 +395,90 @@ function normalizeDateTime(value) {
|
|
|
263
395
|
throw new TypeError('vital_sign_measured_at_invalid');
|
|
264
396
|
return date.toISOString();
|
|
265
397
|
}
|
|
398
|
+
function normalizeEventTiming(value) {
|
|
399
|
+
if (value === 'MORN' || value === 'AFT' || value === 'EVE' || value === 'NIGHT')
|
|
400
|
+
return value;
|
|
401
|
+
throw new TypeError('vital_sign_event_timing_invalid');
|
|
402
|
+
}
|
|
403
|
+
function mean(values) {
|
|
404
|
+
if (values.length === 0 || values.some(value => !validPositive(value)))
|
|
405
|
+
throw new TypeError('vital_sign_value_invalid');
|
|
406
|
+
return Math.round((values.reduce((total, value) => total + value, 0) / values.length) * 10) / 10;
|
|
407
|
+
}
|
|
408
|
+
function bloodPressureValue(reading, key) {
|
|
409
|
+
if (!('systolic' in reading) || !('diastolic' in reading))
|
|
410
|
+
throw new TypeError('blood_pressure_values_invalid');
|
|
411
|
+
const value = key === 'systolic' ? reading.systolic : reading.diastolic;
|
|
412
|
+
if (typeof value !== 'number')
|
|
413
|
+
throw new TypeError('blood_pressure_values_invalid');
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
function scalarReadingValue(reading) {
|
|
417
|
+
if (!('value' in reading) || typeof reading.value !== 'number')
|
|
418
|
+
throw new TypeError('vital_sign_value_invalid');
|
|
419
|
+
return reading.value;
|
|
420
|
+
}
|
|
421
|
+
function repeatedScalarUnit(kind, readings) {
|
|
422
|
+
const units = readings.map(reading => 'unit' in reading ? reading.unit : undefined);
|
|
423
|
+
const selected = units.find((unit) => Boolean(unit));
|
|
424
|
+
if (units.some(unit => unit !== undefined && unit !== selected))
|
|
425
|
+
throw new TypeError('vital_sign_repeated_unit_mismatch');
|
|
426
|
+
return scalarDefinition(kind, selected).unit;
|
|
427
|
+
}
|
|
428
|
+
function replacePrimaryClaims(graph, claims) {
|
|
429
|
+
const primary = buildFlatClaimResourceEntry({
|
|
430
|
+
entryId: graph.primary.entryId,
|
|
431
|
+
resourceType: 'Observation',
|
|
432
|
+
claims,
|
|
433
|
+
});
|
|
434
|
+
return freezeObservationGraph(primary, graph.components);
|
|
435
|
+
}
|
|
436
|
+
function freezeDailyDocument(localDate, timeZone, status, composition, observations) {
|
|
437
|
+
const frozenObservations = Object.freeze([...observations]);
|
|
438
|
+
return Object.freeze({
|
|
439
|
+
localDate,
|
|
440
|
+
timeZone,
|
|
441
|
+
status,
|
|
442
|
+
composition,
|
|
443
|
+
observations: frozenObservations,
|
|
444
|
+
entries: Object.freeze([composition, ...frozenObservations.flatMap(graph => graph.entries)]),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function normalizeLocalDate(value) {
|
|
448
|
+
const normalized = value.trim();
|
|
449
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized))
|
|
450
|
+
throw new TypeError('vital_sign_daily_local_date_invalid');
|
|
451
|
+
const parsed = new Date(`${normalized}T00:00:00.000Z`);
|
|
452
|
+
if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== normalized) {
|
|
453
|
+
throw new TypeError('vital_sign_daily_local_date_invalid');
|
|
454
|
+
}
|
|
455
|
+
return normalized;
|
|
456
|
+
}
|
|
457
|
+
function normalizeTimeZone(value) {
|
|
458
|
+
const normalized = value.trim();
|
|
459
|
+
if (!normalized)
|
|
460
|
+
throw new TypeError('vital_sign_daily_time_zone_invalid');
|
|
461
|
+
try {
|
|
462
|
+
new Intl.DateTimeFormat('en', { timeZone: normalized }).format(0);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
throw new TypeError('vital_sign_daily_time_zone_invalid');
|
|
466
|
+
}
|
|
467
|
+
return normalized;
|
|
468
|
+
}
|
|
469
|
+
function localDateAt(dateTime, timeZone) {
|
|
470
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
471
|
+
timeZone,
|
|
472
|
+
year: 'numeric',
|
|
473
|
+
month: '2-digit',
|
|
474
|
+
day: '2-digit',
|
|
475
|
+
}).formatToParts(new Date(dateTime));
|
|
476
|
+
const value = Object.fromEntries(parts.map(part => [part.type, part.value]));
|
|
477
|
+
return `${value.year}-${value.month}-${value.day}`;
|
|
478
|
+
}
|
|
479
|
+
function latestEffectiveDateTime(graphs) {
|
|
480
|
+
return graphs.map(graph => required(graph.primary.claims[ObservationClaim.EffectiveDateTime] ?? '', 'vital_sign_measured_at_required')).sort().at(-1);
|
|
481
|
+
}
|
|
266
482
|
function validPositive(value) {
|
|
267
483
|
return Number.isFinite(value) && value > 0;
|
|
268
484
|
}
|