fhir-data-utils-ts 0.6.9 → 0.6.11
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/CHANGELOG.md +14 -0
- package/README.md +6 -0
- package/dist/clinical-artifact-editor.d.ts +17 -0
- package/dist/clinical-artifact-editor.js +75 -0
- package/dist/data/ips-all-sections.data.d.ts +14 -14
- package/dist/data/ips-clinical-artifact-schemas.data.d.ts +2 -1
- package/dist/data/ips-clinical-artifact-schemas.data.js +5 -0
- package/dist/fhir-temporal-presentation.d.ts +27 -0
- package/dist/fhir-temporal-presentation.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/docs/UHC_FHIR_UTILS_MIGRATION.md +13 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Add the shared clinical-artifact editor boundary for native FHIR R4 imports:
|
|
6
|
+
hydrate only schema-owned fields, materialize supported IPS resources from
|
|
7
|
+
editor plus host-owned claims, and prove FHIR-to-form-to-FHIR semantic
|
|
8
|
+
round trips for every reusable IPS authoring schema.
|
|
9
|
+
- Give every reusable IPS authoring schema canonical initial claims so portal
|
|
10
|
+
components do not invent resource status defaults.
|
|
11
|
+
|
|
12
|
+
- Recover the useful locale-aware intent of the historical UHC
|
|
13
|
+
internationalization helpers as strict FHIR temporal presentation tools.
|
|
14
|
+
Date-only values retain their calendar day, instants render with the user's
|
|
15
|
+
locale and IANA time zone, editor date/time fields compose a canonical FHIR
|
|
16
|
+
instant with optional seconds defaulted to `00`, and inclusive ranges treat
|
|
17
|
+
an omitted upper bound as the supplied current instant.
|
|
18
|
+
|
|
5
19
|
- Preserve locally entered `Observation.code.text` when a terminology code has
|
|
6
20
|
not yet been confirmed, so profile-aware authoring remains visible and can be
|
|
7
21
|
coded later without inventing a code.
|
package/README.md
CHANGED
|
@@ -98,6 +98,12 @@ derived whole-second value; retain the original timezone-bearing instant and
|
|
|
98
98
|
an independent `Encounter.id`. See
|
|
99
99
|
[`docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md`](docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md).
|
|
100
100
|
|
|
101
|
+
Use the `/fhir-temporal-presentation` entry point at UI boundaries. It formats
|
|
102
|
+
FHIR date, dateTime and instant values with the user's locale and IANA time
|
|
103
|
+
zone, composes separate `YYYY-MM-DD` and `HH:mm[:ss]` editor fields, and
|
|
104
|
+
evaluates inclusive date-time ranges. ISO values remain the wire/storage form;
|
|
105
|
+
they are not user-facing labels.
|
|
106
|
+
|
|
101
107
|
No consumer should copy claim paths or rebuild these structures with literals.
|
|
102
108
|
Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
|
|
103
109
|
`/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FhirIpsClinicalArtifactSchema, type FhirIpsClinicalArtifactType } from './data/ips-clinical-artifact-schemas.data.js';
|
|
2
|
+
export type ClinicalArtifactFhirR4Resource = Readonly<Record<string, unknown> & {
|
|
3
|
+
resourceType: FhirIpsClinicalArtifactType;
|
|
4
|
+
}>;
|
|
5
|
+
/** Host-owned subject claims added only at the trusted materialization boundary. */
|
|
6
|
+
export declare function clinicalArtifactSubjectClaims(resourceType: FhirIpsClinicalArtifactType, subjectReference: string): Readonly<Record<string, string>>;
|
|
7
|
+
/**
|
|
8
|
+
* Hydrates one schema-driven editor from a native FHIR R4 resource.
|
|
9
|
+
*
|
|
10
|
+
* Only claims explicitly owned by the selected authoring schema are returned;
|
|
11
|
+
* subject, provenance and authorization claims remain host-owned.
|
|
12
|
+
*/
|
|
13
|
+
export declare function clinicalArtifactEditorClaimsFromFhirR4(resource: ClinicalArtifactFhirR4Resource, schema: FhirIpsClinicalArtifactSchema): Readonly<Record<string, string>>;
|
|
14
|
+
/** Materializes a native FHIR R4 resource at the explicit editor/import boundary. */
|
|
15
|
+
export declare function clinicalArtifactFhirR4FromEditorClaims(resourceType: FhirIpsClinicalArtifactType, editorClaims: Readonly<Record<string, unknown>>, hostClaims?: Readonly<Record<string, unknown>>): ClinicalArtifactFhirR4Resource;
|
|
16
|
+
/** Converts a supported native FHIR R4 resource to its canonical flat claims. */
|
|
17
|
+
export declare function clinicalArtifactFhirR4ToFlat(resource: ClinicalArtifactFhirR4Resource): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
|
|
2
|
+
import { allergyIntoleranceFhirR4ToFlat, allergyIntoleranceFlatToFhirR4, conditionFhirR4ToFlat, conditionFlatToFhirR4, immunizationFhirR4ToFlat, immunizationFlatToFhirR4, medicationStatementFhirR4ToFlat, medicationStatementFlatToFhirR4, observationFromFlatToFhirR4, observationToFlatFhirR4, procedureFhirR4ToFlat, procedureFlatToFhirR4, } from './converters/index.js';
|
|
3
|
+
import { FhirIpsClinicalArtifactTypes, } from './data/ips-clinical-artifact-schemas.data.js';
|
|
4
|
+
import { AllergyIntoleranceClaim } from './claims/allergy-intolerance-claims.js';
|
|
5
|
+
import { ConditionClaim } from './claims/condition-claims.js';
|
|
6
|
+
import { ImmunizationClaim } from './claims/immunization-claims.js';
|
|
7
|
+
import { MedicationStatementClaim } from './claims/medication-statement-claims.js';
|
|
8
|
+
import { ProcedureClaim } from './claims/procedure-claims.js';
|
|
9
|
+
import { ObservationClaim } from './observation-claims.js';
|
|
10
|
+
/** Host-owned subject claims added only at the trusted materialization boundary. */
|
|
11
|
+
export function clinicalArtifactSubjectClaims(resourceType, subjectReference) {
|
|
12
|
+
const claim = {
|
|
13
|
+
[FhirIpsClinicalArtifactTypes.AllergyIntolerance]: AllergyIntoleranceClaim.Subject,
|
|
14
|
+
[FhirIpsClinicalArtifactTypes.MedicationStatement]: MedicationStatementClaim.Subject,
|
|
15
|
+
[FhirIpsClinicalArtifactTypes.Immunization]: ImmunizationClaim.Subject,
|
|
16
|
+
[FhirIpsClinicalArtifactTypes.Condition]: ConditionClaim.Subject,
|
|
17
|
+
[FhirIpsClinicalArtifactTypes.Procedure]: ProcedureClaim.Subject,
|
|
18
|
+
[FhirIpsClinicalArtifactTypes.Observation]: ObservationClaim.Subject,
|
|
19
|
+
}[resourceType];
|
|
20
|
+
return Object.freeze({ [claim]: subjectReference });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Hydrates one schema-driven editor from a native FHIR R4 resource.
|
|
24
|
+
*
|
|
25
|
+
* Only claims explicitly owned by the selected authoring schema are returned;
|
|
26
|
+
* subject, provenance and authorization claims remain host-owned.
|
|
27
|
+
*/
|
|
28
|
+
export function clinicalArtifactEditorClaimsFromFhirR4(resource, schema) {
|
|
29
|
+
if (resource.resourceType !== schema.resourceType) {
|
|
30
|
+
throw new TypeError('clinical_artifact_schema_resource_type_mismatch');
|
|
31
|
+
}
|
|
32
|
+
const acceptedClaims = new Set([...schema.requiredClaims, ...schema.optionalClaims]);
|
|
33
|
+
const flatClaims = clinicalArtifactFhirR4ToFlat(resource);
|
|
34
|
+
const definedFlatClaims = Object.fromEntries(Object.entries(flatClaims).filter(([, value]) => value !== undefined && value !== null && value !== ''));
|
|
35
|
+
return Object.freeze(Object.fromEntries(Object.entries({ ...schema.initialClaims, ...definedFlatClaims })
|
|
36
|
+
.filter(([claim, value]) => acceptedClaims.has(claim) && value !== undefined && value !== null && value !== '')
|
|
37
|
+
.map(([claim, value]) => [claim, String(value)])));
|
|
38
|
+
}
|
|
39
|
+
/** Materializes a native FHIR R4 resource at the explicit editor/import boundary. */
|
|
40
|
+
export function clinicalArtifactFhirR4FromEditorClaims(resourceType, editorClaims, hostClaims = {}) {
|
|
41
|
+
const normalizedClaims = Object.fromEntries(Object.entries({ ...hostClaims, ...editorClaims })
|
|
42
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
43
|
+
.map(([claim, value]) => [claim, String(value)]));
|
|
44
|
+
switch (resourceType) {
|
|
45
|
+
case FhirIpsClinicalArtifactTypes.AllergyIntolerance:
|
|
46
|
+
return allergyIntoleranceFlatToFhirR4(normalizedClaims);
|
|
47
|
+
case FhirIpsClinicalArtifactTypes.MedicationStatement:
|
|
48
|
+
return medicationStatementFlatToFhirR4(normalizedClaims);
|
|
49
|
+
case FhirIpsClinicalArtifactTypes.Immunization:
|
|
50
|
+
return immunizationFlatToFhirR4(normalizedClaims);
|
|
51
|
+
case FhirIpsClinicalArtifactTypes.Condition:
|
|
52
|
+
return conditionFlatToFhirR4(normalizedClaims);
|
|
53
|
+
case FhirIpsClinicalArtifactTypes.Procedure:
|
|
54
|
+
return procedureFlatToFhirR4(normalizedClaims);
|
|
55
|
+
case FhirIpsClinicalArtifactTypes.Observation:
|
|
56
|
+
return observationFromFlatToFhirR4(normalizedClaims);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Converts a supported native FHIR R4 resource to its canonical flat claims. */
|
|
60
|
+
export function clinicalArtifactFhirR4ToFlat(resource) {
|
|
61
|
+
switch (resource.resourceType) {
|
|
62
|
+
case FhirIpsClinicalArtifactTypes.AllergyIntolerance:
|
|
63
|
+
return allergyIntoleranceFhirR4ToFlat(resource);
|
|
64
|
+
case FhirIpsClinicalArtifactTypes.MedicationStatement:
|
|
65
|
+
return medicationStatementFhirR4ToFlat(resource);
|
|
66
|
+
case FhirIpsClinicalArtifactTypes.Immunization:
|
|
67
|
+
return immunizationFhirR4ToFlat(resource);
|
|
68
|
+
case FhirIpsClinicalArtifactTypes.Condition:
|
|
69
|
+
return conditionFhirR4ToFlat(resource);
|
|
70
|
+
case FhirIpsClinicalArtifactTypes.Procedure:
|
|
71
|
+
return procedureFhirR4ToFlat(resource);
|
|
72
|
+
case FhirIpsClinicalArtifactTypes.Observation:
|
|
73
|
+
return observationToFlatFhirR4(resource);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -67,7 +67,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
67
67
|
display: string;
|
|
68
68
|
}[];
|
|
69
69
|
};
|
|
70
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
70
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
71
71
|
id: string;
|
|
72
72
|
status: string;
|
|
73
73
|
subject: {
|
|
@@ -84,7 +84,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
84
84
|
}[];
|
|
85
85
|
};
|
|
86
86
|
effectiveDateTime: string;
|
|
87
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
87
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
88
88
|
id: string;
|
|
89
89
|
subject: {
|
|
90
90
|
reference: string;
|
|
@@ -103,7 +103,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
103
103
|
}[];
|
|
104
104
|
};
|
|
105
105
|
occurrenceDateTime: string;
|
|
106
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
106
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
107
107
|
id: string;
|
|
108
108
|
subject: {
|
|
109
109
|
reference: string;
|
|
@@ -119,7 +119,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
119
119
|
}[];
|
|
120
120
|
};
|
|
121
121
|
effectiveDateTime: string;
|
|
122
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
122
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
123
123
|
id: string;
|
|
124
124
|
subject: {
|
|
125
125
|
reference: string;
|
|
@@ -135,7 +135,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
135
135
|
}[];
|
|
136
136
|
};
|
|
137
137
|
performedDateTime: string;
|
|
138
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
138
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
139
139
|
id: string;
|
|
140
140
|
subject: {
|
|
141
141
|
reference: string;
|
|
@@ -145,7 +145,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
145
145
|
device: {
|
|
146
146
|
display: "Corrective lens use";
|
|
147
147
|
};
|
|
148
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
148
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
149
149
|
id: string;
|
|
150
150
|
subject: {
|
|
151
151
|
reference: string;
|
|
@@ -166,7 +166,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
166
166
|
system: string;
|
|
167
167
|
code: string;
|
|
168
168
|
};
|
|
169
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
169
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
170
170
|
id: string;
|
|
171
171
|
status: string;
|
|
172
172
|
subject: {
|
|
@@ -190,7 +190,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
190
190
|
display: string;
|
|
191
191
|
}[];
|
|
192
192
|
};
|
|
193
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
193
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
194
194
|
id: string;
|
|
195
195
|
status: string;
|
|
196
196
|
subject: {
|
|
@@ -206,7 +206,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
206
206
|
display: string;
|
|
207
207
|
}[];
|
|
208
208
|
};
|
|
209
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
209
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
210
210
|
id: string;
|
|
211
211
|
subject: {
|
|
212
212
|
reference: string;
|
|
@@ -221,7 +221,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
221
221
|
display: string;
|
|
222
222
|
}[];
|
|
223
223
|
};
|
|
224
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
224
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
225
225
|
id: string;
|
|
226
226
|
status: string;
|
|
227
227
|
subject: {
|
|
@@ -248,7 +248,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
248
248
|
patient: {
|
|
249
249
|
reference: string;
|
|
250
250
|
};
|
|
251
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
251
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
252
252
|
id: string;
|
|
253
253
|
subject: {
|
|
254
254
|
reference: string;
|
|
@@ -257,7 +257,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
257
257
|
status: string;
|
|
258
258
|
description: "Independent daily activities";
|
|
259
259
|
summary: "Independent daily activities";
|
|
260
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
260
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
261
261
|
id: string;
|
|
262
262
|
subject: {
|
|
263
263
|
reference: string;
|
|
@@ -274,7 +274,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
274
274
|
display: string;
|
|
275
275
|
}[];
|
|
276
276
|
}[];
|
|
277
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
277
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
278
278
|
id: string;
|
|
279
279
|
subject: {
|
|
280
280
|
reference: string;
|
|
@@ -291,7 +291,7 @@ export declare const FHIR_IPS_ALL_SECTIONS_DEMO_DATA: Readonly<{
|
|
|
291
291
|
display: string;
|
|
292
292
|
}[];
|
|
293
293
|
};
|
|
294
|
-
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "
|
|
294
|
+
resourceType: "Condition" | "Goal" | "AllergyIntolerance" | "CarePlan" | "ClinicalImpression" | "Consent" | "DeviceUseStatement" | "DiagnosticReport" | "Flag" | "Immunization" | "MedicationStatement" | "Observation" | "Procedure";
|
|
295
295
|
id: string;
|
|
296
296
|
status: string;
|
|
297
297
|
subject: {
|
|
@@ -14,6 +14,8 @@ export type FhirIpsClinicalArtifactSchema = Readonly<{
|
|
|
14
14
|
optionalClaims: readonly string[];
|
|
15
15
|
/** Source-text claims for which a user may explicitly confirm a terminology candidate. */
|
|
16
16
|
terminologyTextClaims: readonly string[];
|
|
17
|
+
/** Canonical initial values used by every schema-driven editor. */
|
|
18
|
+
initialClaims: Readonly<Record<string, string>>;
|
|
17
19
|
}>;
|
|
18
20
|
export declare const FhirIpsObservationArtifactKinds: Readonly<{
|
|
19
21
|
readonly VitalSigns: "vital-signs";
|
|
@@ -24,7 +26,6 @@ export declare const FhirIpsObservationArtifactKinds: Readonly<{
|
|
|
24
26
|
export type FhirIpsObservationArtifactKind = typeof FhirIpsObservationArtifactKinds[keyof typeof FhirIpsObservationArtifactKinds];
|
|
25
27
|
export type FhirIpsObservationArtifactSchema = FhirIpsClinicalArtifactSchema & Readonly<{
|
|
26
28
|
kind: FhirIpsObservationArtifactKind;
|
|
27
|
-
initialClaims: Readonly<Record<string, string>>;
|
|
28
29
|
}>;
|
|
29
30
|
/** Section-specific Observation authoring contracts; a resource type alone is not a sufficient selector. */
|
|
30
31
|
export declare const FHIR_IPS_OBSERVATION_ARTIFACT_SCHEMAS: Readonly<Record<FhirIpsObservationArtifactKind, FhirIpsObservationArtifactSchema>>;
|
|
@@ -83,6 +83,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
|
|
|
83
83
|
requiredClaims: Object.freeze([AllergyIntoleranceClaim.CodeText, AllergyIntoleranceClaim.ClinicalStatus]),
|
|
84
84
|
optionalClaims: Object.freeze([AllergyIntoleranceClaim.Code, AllergyIntoleranceClaim.CodeDisplay, AllergyIntoleranceClaim.Category, AllergyIntoleranceClaim.VerificationStatus, AllergyIntoleranceClaim.Criticality, AllergyIntoleranceClaim.OnsetDateTime, AllergyIntoleranceClaim.Recorder]),
|
|
85
85
|
terminologyTextClaims: Object.freeze([AllergyIntoleranceClaim.CodeText]),
|
|
86
|
+
initialClaims: Object.freeze({ [AllergyIntoleranceClaim.ClinicalStatus]: 'active' }),
|
|
86
87
|
}),
|
|
87
88
|
[FhirIpsClinicalArtifactTypes.MedicationStatement]: Object.freeze({
|
|
88
89
|
resourceType: FhirIpsClinicalArtifactTypes.MedicationStatement,
|
|
@@ -90,6 +91,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
|
|
|
90
91
|
requiredClaims: Object.freeze([MedicationStatementClaim.CodeText, MedicationStatementClaim.Status]),
|
|
91
92
|
optionalClaims: Object.freeze([MedicationStatementClaim.Code, MedicationStatementClaim.CodeDisplay, MedicationStatementClaim.Category, MedicationStatementClaim.Effective, MedicationStatementClaim.DosageInstruction, MedicationStatementClaim.DosageRoute, MedicationStatementClaim.TimingFrequency, MedicationStatementClaim.TimingPeriod, MedicationStatementClaim.TimingPeriodUnit, MedicationStatementClaim.Note]),
|
|
92
93
|
terminologyTextClaims: Object.freeze([MedicationStatementClaim.CodeText]),
|
|
94
|
+
initialClaims: Object.freeze({ [MedicationStatementClaim.Status]: 'active' }),
|
|
93
95
|
}),
|
|
94
96
|
[FhirIpsClinicalArtifactTypes.Immunization]: Object.freeze({
|
|
95
97
|
resourceType: FhirIpsClinicalArtifactTypes.Immunization,
|
|
@@ -97,6 +99,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
|
|
|
97
99
|
requiredClaims: Object.freeze([ImmunizationClaim.VaccineCodeText, ImmunizationClaim.Status]),
|
|
98
100
|
optionalClaims: Object.freeze([ImmunizationClaim.VaccineCode, ImmunizationClaim.VaccineCodeDisplay, ImmunizationClaim.Date, ImmunizationClaim.LotNumber, ImmunizationClaim.DoseSequence, ImmunizationClaim.Manufacturer, ImmunizationClaim.Performer, ImmunizationClaim.Location, ImmunizationClaim.Route, ImmunizationClaim.RouteDisplay, ImmunizationClaim.Site, ImmunizationClaim.SiteDisplay, ImmunizationClaim.Series, ImmunizationClaim.TargetDisease, ImmunizationClaim.TargetDiseaseText, ImmunizationClaim.TargetDiseaseDisplay, ImmunizationClaim.ReactionDate, ImmunizationClaim.Note]),
|
|
99
101
|
terminologyTextClaims: Object.freeze([ImmunizationClaim.VaccineCodeText, ImmunizationClaim.TargetDiseaseText]),
|
|
102
|
+
initialClaims: Object.freeze({ [ImmunizationClaim.Status]: 'completed' }),
|
|
100
103
|
}),
|
|
101
104
|
[FhirIpsClinicalArtifactTypes.Condition]: Object.freeze({
|
|
102
105
|
resourceType: FhirIpsClinicalArtifactTypes.Condition,
|
|
@@ -104,6 +107,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
|
|
|
104
107
|
requiredClaims: Object.freeze([ConditionClaim.CodeText, ConditionClaim.ClinicalStatus]),
|
|
105
108
|
optionalClaims: Object.freeze([ConditionClaim.Code, ConditionClaim.CodeDisplay, ConditionClaim.Category, ConditionClaim.VerificationStatus, ConditionClaim.Severity, ConditionClaim.OnsetDateTime, ConditionClaim.RecordedDate, ConditionClaim.Asserter, ConditionClaim.Recorder]),
|
|
106
109
|
terminologyTextClaims: Object.freeze([ConditionClaim.CodeText]),
|
|
110
|
+
initialClaims: Object.freeze({ [ConditionClaim.ClinicalStatus]: 'active' }),
|
|
107
111
|
}),
|
|
108
112
|
[FhirIpsClinicalArtifactTypes.Procedure]: Object.freeze({
|
|
109
113
|
resourceType: FhirIpsClinicalArtifactTypes.Procedure,
|
|
@@ -111,6 +115,7 @@ export const FHIR_IPS_CLINICAL_ARTIFACT_SCHEMAS = Object.freeze({
|
|
|
111
115
|
requiredClaims: Object.freeze([ProcedureClaim.CodeText, ProcedureClaim.Status]),
|
|
112
116
|
optionalClaims: Object.freeze([ProcedureClaim.Code, ProcedureClaim.CodeDisplay, ProcedureClaim.Date, ProcedureClaim.Performer, ProcedureClaim.Location, ProcedureClaim.BodySite, ProcedureClaim.ReasonCode, ProcedureClaim.ReasonReference, ProcedureClaim.Note]),
|
|
113
117
|
terminologyTextClaims: Object.freeze([ProcedureClaim.CodeText]),
|
|
118
|
+
initialClaims: Object.freeze({ [ProcedureClaim.Status]: 'completed' }),
|
|
114
119
|
}),
|
|
115
120
|
[FhirIpsClinicalArtifactTypes.Observation]: Object.freeze({
|
|
116
121
|
...FHIR_IPS_OBSERVATION_ARTIFACT_SCHEMAS[FhirIpsObservationArtifactKinds.VitalSigns],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type FhirTemporalRange = Readonly<{
|
|
2
|
+
from?: string;
|
|
3
|
+
to?: string;
|
|
4
|
+
}>;
|
|
5
|
+
export type LocalDateTimeParts = Readonly<{
|
|
6
|
+
date: string;
|
|
7
|
+
time: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type ComposeFhirDateTimeInput = LocalDateTimeParts & Readonly<{
|
|
10
|
+
timeZone: string;
|
|
11
|
+
}>;
|
|
12
|
+
export type FhirTemporalFormatOptions = Readonly<{
|
|
13
|
+
timeZone?: string;
|
|
14
|
+
includeTime?: boolean;
|
|
15
|
+
includeSeconds?: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Formats a FHIR date, dateTime or instant for a human in their locale.
|
|
19
|
+
* Wire values remain unchanged; ISO syntax is never used as presentation.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatFhirTemporalForLocale(value: string | Date, locale: string, options?: FhirTemporalFormatOptions): string;
|
|
22
|
+
/** Builds a FHIR instant from separate browser-friendly local date and time fields. */
|
|
23
|
+
export declare function composeFhirDateTimeFromLocalParts(input: ComposeFhirDateTimeInput): string;
|
|
24
|
+
/** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
|
|
25
|
+
export declare function splitFhirDateTimeForInput(value: string | Date, timeZone: string): LocalDateTimeParts;
|
|
26
|
+
/** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
|
|
27
|
+
export declare function isFhirTemporalWithinRange(value: string | Date, range: FhirTemporalRange, now?: Date): boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2
|
+
const TIME_PATTERN = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
3
|
+
/**
|
|
4
|
+
* Formats a FHIR date, dateTime or instant for a human in their locale.
|
|
5
|
+
* Wire values remain unchanged; ISO syntax is never used as presentation.
|
|
6
|
+
*/
|
|
7
|
+
export function formatFhirTemporalForLocale(value, locale, options = {}) {
|
|
8
|
+
const dateOnly = typeof value === 'string' ? DATE_PATTERN.exec(value) : null;
|
|
9
|
+
const includeTime = options.includeTime ?? !dateOnly;
|
|
10
|
+
const date = dateOnly
|
|
11
|
+
? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])))
|
|
12
|
+
: value instanceof Date ? value : new Date(value);
|
|
13
|
+
if (Number.isNaN(date.getTime()))
|
|
14
|
+
throw new RangeError('FHIR temporal value is invalid');
|
|
15
|
+
return new Intl.DateTimeFormat(locale, {
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: dateOnly ? '2-digit' : 'short',
|
|
18
|
+
day: dateOnly ? '2-digit' : 'numeric',
|
|
19
|
+
hour: includeTime ? '2-digit' : undefined,
|
|
20
|
+
minute: includeTime ? '2-digit' : undefined,
|
|
21
|
+
second: includeTime && options.includeSeconds ? '2-digit' : undefined,
|
|
22
|
+
hour12: includeTime ? false : undefined,
|
|
23
|
+
timeZone: dateOnly ? 'UTC' : options.timeZone,
|
|
24
|
+
}).format(date);
|
|
25
|
+
}
|
|
26
|
+
/** Builds a FHIR instant from separate browser-friendly local date and time fields. */
|
|
27
|
+
export function composeFhirDateTimeFromLocalParts(input) {
|
|
28
|
+
const dateMatch = DATE_PATTERN.exec(input.date);
|
|
29
|
+
const timeMatch = TIME_PATTERN.exec(input.time);
|
|
30
|
+
if (!dateMatch || !timeMatch)
|
|
31
|
+
throw new RangeError('Use YYYY-MM-DD and HH:mm or HH:mm:ss');
|
|
32
|
+
const expected = {
|
|
33
|
+
year: Number(dateMatch[1]),
|
|
34
|
+
month: Number(dateMatch[2]),
|
|
35
|
+
day: Number(dateMatch[3]),
|
|
36
|
+
hour: Number(timeMatch[1]),
|
|
37
|
+
minute: Number(timeMatch[2]),
|
|
38
|
+
second: Number(timeMatch[3] ?? '00'),
|
|
39
|
+
};
|
|
40
|
+
assertValidCalendarParts(expected);
|
|
41
|
+
assertTimeZone(input.timeZone);
|
|
42
|
+
const expectedUtc = Date.UTC(expected.year, expected.month - 1, expected.day, expected.hour, expected.minute, expected.second);
|
|
43
|
+
let candidate = expectedUtc;
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
45
|
+
const actual = datePartsInTimeZone(new Date(candidate), input.timeZone);
|
|
46
|
+
const actualUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second);
|
|
47
|
+
candidate += expectedUtc - actualUtc;
|
|
48
|
+
}
|
|
49
|
+
const result = new Date(candidate);
|
|
50
|
+
const roundTrip = datePartsInTimeZone(result, input.timeZone);
|
|
51
|
+
if (Object.keys(expected).some((key) => expected[key] !== roundTrip[key])) {
|
|
52
|
+
throw new RangeError('Local date and time do not exist in the selected time zone');
|
|
53
|
+
}
|
|
54
|
+
return result.toISOString();
|
|
55
|
+
}
|
|
56
|
+
/** Splits a FHIR instant into local inputs without leaking ISO UTC presentation. */
|
|
57
|
+
export function splitFhirDateTimeForInput(value, timeZone) {
|
|
58
|
+
assertTimeZone(timeZone);
|
|
59
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
60
|
+
if (Number.isNaN(date.getTime()))
|
|
61
|
+
throw new RangeError('FHIR temporal value is invalid');
|
|
62
|
+
const parts = datePartsInTimeZone(date, timeZone);
|
|
63
|
+
return {
|
|
64
|
+
date: `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`,
|
|
65
|
+
time: `${pad(parts.hour)}:${pad(parts.minute)}:${pad(parts.second)}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Applies an inclusive range; an omitted upper bound means the supplied current instant. */
|
|
69
|
+
export function isFhirTemporalWithinRange(value, range, now = new Date()) {
|
|
70
|
+
const instant = value instanceof Date ? value : new Date(value);
|
|
71
|
+
const from = range.from ? new Date(range.from) : undefined;
|
|
72
|
+
const to = range.to ? new Date(range.to) : now;
|
|
73
|
+
if ([instant, from, to].some((date) => date && Number.isNaN(date.getTime()))) {
|
|
74
|
+
throw new RangeError('FHIR temporal range is invalid');
|
|
75
|
+
}
|
|
76
|
+
return (!from || instant >= from) && instant <= to;
|
|
77
|
+
}
|
|
78
|
+
function datePartsInTimeZone(date, timeZone) {
|
|
79
|
+
const values = new Map(new Intl.DateTimeFormat('en-CA', {
|
|
80
|
+
timeZone,
|
|
81
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
82
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
|
83
|
+
}).formatToParts(date).map(({ type, value }) => [type, value]));
|
|
84
|
+
return {
|
|
85
|
+
year: Number(values.get('year')),
|
|
86
|
+
month: Number(values.get('month')),
|
|
87
|
+
day: Number(values.get('day')),
|
|
88
|
+
hour: Number(values.get('hour')),
|
|
89
|
+
minute: Number(values.get('minute')),
|
|
90
|
+
second: Number(values.get('second')),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function assertValidCalendarParts(parts) {
|
|
94
|
+
if (parts.hour > 23 || parts.minute > 59 || parts.second > 59)
|
|
95
|
+
throw new RangeError('Local time is invalid');
|
|
96
|
+
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
97
|
+
if (date.getUTCFullYear() !== parts.year || date.getUTCMonth() + 1 !== parts.month || date.getUTCDate() !== parts.day) {
|
|
98
|
+
throw new RangeError('Local date is invalid');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function assertTimeZone(timeZone) {
|
|
102
|
+
try {
|
|
103
|
+
new Intl.DateTimeFormat('en', { timeZone }).format(0);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new RangeError('IANA time zone is invalid');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function pad(value) {
|
|
110
|
+
return String(value).padStart(2, '0');
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,3 +27,5 @@ export * from './healthcare-service-specialty-map.js';
|
|
|
27
27
|
export * from './data/index.js';
|
|
28
28
|
export * from './fhir-instant.js';
|
|
29
29
|
export * from './health-credential-qr.js';
|
|
30
|
+
export * from './fhir-temporal-presentation.js';
|
|
31
|
+
export * from './clinical-artifact-editor.js';
|
package/dist/index.js
CHANGED
|
@@ -27,3 +27,5 @@ export * from './healthcare-service-specialty-map.js';
|
|
|
27
27
|
export * from './data/index.js';
|
|
28
28
|
export * from './fhir-instant.js';
|
|
29
29
|
export * from './health-credential-qr.js';
|
|
30
|
+
export * from './fhir-temporal-presentation.js';
|
|
31
|
+
export * from './clinical-artifact-editor.js';
|
|
@@ -154,6 +154,19 @@ Converters and builders import catalog symbols. Tests may spell native FHIR
|
|
|
154
154
|
wire fields, but do not duplicate flat-claim paths except when proving that a
|
|
155
155
|
malformed or invented path is rejected.
|
|
156
156
|
|
|
157
|
+
## Schema-driven editor boundary
|
|
158
|
+
|
|
159
|
+
`clinicalArtifactFhirR4ToFlat(...)` and
|
|
160
|
+
`clinicalArtifactEditorClaimsFromFhirR4(...)` are import/read adapters. They
|
|
161
|
+
turn a native FHIR R4 clinical resource into canonical flat claims and never
|
|
162
|
+
own a portal form, authorization or transport decision.
|
|
163
|
+
|
|
164
|
+
The cross-channel JSON Schema and the `BundleEditor`/`BundleReader` round trip
|
|
165
|
+
belong to `sos-data-utils-ts`. A shared React component consumes both layers:
|
|
166
|
+
FHIR conversion for native imports, then the SOS JSON Schema to select and
|
|
167
|
+
validate editable values and create the claims-first changes Bundle. Portals
|
|
168
|
+
must not add resource-specific claim mappers between those shared layers.
|
|
169
|
+
|
|
157
170
|
## Subscription notification boundary
|
|
158
171
|
|
|
159
172
|
FHIR R5 `SubscriptionTopic` and `Subscription` are native configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhir-data-utils-ts",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
4
4
|
"description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -111,6 +111,10 @@
|
|
|
111
111
|
"types": "./dist/health-credential-qr.d.ts",
|
|
112
112
|
"default": "./dist/health-credential-qr.js"
|
|
113
113
|
},
|
|
114
|
+
"./fhir-temporal-presentation": {
|
|
115
|
+
"types": "./dist/fhir-temporal-presentation.d.ts",
|
|
116
|
+
"default": "./dist/fhir-temporal-presentation.js"
|
|
117
|
+
},
|
|
114
118
|
"./research-study-r5": {
|
|
115
119
|
"types": "./dist/research-study-r5.d.ts",
|
|
116
120
|
"default": "./dist/research-study-r5.js"
|