@peerbits/fhir-validator 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/base-definitions/bundle.d.ts +2 -0
- package/dist/base-definitions/bundle.js +41 -0
- package/dist/base-definitions/claim-response.d.ts +5 -0
- package/dist/base-definitions/claim-response.js +186 -0
- package/dist/base-definitions/claim.d.ts +5 -0
- package/dist/base-definitions/claim.js +240 -0
- package/dist/base-definitions/condition.d.ts +5 -0
- package/dist/base-definitions/condition.js +78 -0
- package/dist/base-definitions/coverage.d.ts +5 -0
- package/dist/base-definitions/coverage.js +109 -0
- package/dist/base-definitions/encounter.d.ts +5 -0
- package/dist/base-definitions/encounter.js +137 -0
- package/dist/base-definitions/observation.d.ts +5 -0
- package/dist/base-definitions/observation.js +240 -0
- package/dist/base-definitions/patient.d.ts +5 -0
- package/dist/base-definitions/patient.js +163 -0
- package/dist/coding.d.ts +17 -0
- package/dist/coding.js +139 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +22 -0
- package/dist/profiles/us-core-observation-vitals.d.ts +11 -0
- package/dist/profiles/us-core-observation-vitals.js +22 -0
- package/dist/profiles/us-core-patient.d.ts +10 -0
- package/dist/profiles/us-core-patient.js +21 -0
- package/dist/reference-rules.d.ts +14 -0
- package/dist/reference-rules.js +139 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +5 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +214 -0
- package/package.json +6 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { validateCodeableConcept } from "../coding.js";
|
|
2
|
+
import { validateReference } from "../reference-rules.js";
|
|
3
|
+
const VALID_COVERAGE_STATUSES = ["active", "cancelled", "draft", "entered-in-error"];
|
|
4
|
+
/**
|
|
5
|
+
* Validates base structural rules for FHIR R4 Coverage resource.
|
|
6
|
+
*/
|
|
7
|
+
export function validateCoverage(resource, path = "Coverage") {
|
|
8
|
+
const issues = [];
|
|
9
|
+
if (resource.resourceType !== "Coverage") {
|
|
10
|
+
issues.push({
|
|
11
|
+
severity: "error",
|
|
12
|
+
path: `${path}.resourceType`,
|
|
13
|
+
code: "invalid-resource-type",
|
|
14
|
+
message: `Expected resourceType 'Coverage', found '${String(resource.resourceType)}'.`,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// status (Required)
|
|
18
|
+
if (resource.status === undefined || resource.status === null || resource.status === "") {
|
|
19
|
+
issues.push({
|
|
20
|
+
severity: "error",
|
|
21
|
+
path: `${path}.status`,
|
|
22
|
+
code: "required",
|
|
23
|
+
message: `Missing required 'status' in '${path}'.`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else if (typeof resource.status !== "string" || !VALID_COVERAGE_STATUSES.includes(resource.status)) {
|
|
27
|
+
issues.push({
|
|
28
|
+
severity: "error",
|
|
29
|
+
path: `${path}.status`,
|
|
30
|
+
code: "invalid-value",
|
|
31
|
+
message: `Invalid Coverage status '${String(resource.status)}'. Allowed values: ${VALID_COVERAGE_STATUSES.join(", ")}.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// beneficiary (Required Reference: Patient)
|
|
35
|
+
if (resource.beneficiary === undefined || resource.beneficiary === null) {
|
|
36
|
+
issues.push({
|
|
37
|
+
severity: "error",
|
|
38
|
+
path: `${path}.beneficiary`,
|
|
39
|
+
code: "required",
|
|
40
|
+
message: `Missing required 'beneficiary' in '${path}'.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
issues.push(...validateReference(resource.beneficiary, `${path}.beneficiary`, ["Patient"]));
|
|
45
|
+
}
|
|
46
|
+
// payor (Required Reference[]: Organization | Patient | RelatedPerson, min 1)
|
|
47
|
+
if (resource.payor === undefined || resource.payor === null) {
|
|
48
|
+
issues.push({
|
|
49
|
+
severity: "error",
|
|
50
|
+
path: `${path}.payor`,
|
|
51
|
+
code: "required",
|
|
52
|
+
message: `Missing required 'payor' in '${path}'.`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (!Array.isArray(resource.payor)) {
|
|
56
|
+
issues.push({
|
|
57
|
+
severity: "error",
|
|
58
|
+
path: `${path}.payor`,
|
|
59
|
+
code: "invalid-type",
|
|
60
|
+
message: `Expected '${path}.payor' to be an array, got ${typeof resource.payor}.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (resource.payor.length === 0) {
|
|
64
|
+
issues.push({
|
|
65
|
+
severity: "error",
|
|
66
|
+
path: `${path}.payor`,
|
|
67
|
+
code: "cardinality",
|
|
68
|
+
message: `Expected at least 1 payor in '${path}.payor', found 0.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resource.payor.forEach((p, idx) => {
|
|
73
|
+
issues.push(...validateReference(p, `${path}.payor[${idx}]`, ["Organization", "Patient", "RelatedPerson"]));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// subscriber (Reference: Patient | RelatedPerson)
|
|
77
|
+
if (resource.subscriber !== undefined) {
|
|
78
|
+
issues.push(...validateReference(resource.subscriber, `${path}.subscriber`, ["Patient", "RelatedPerson"]));
|
|
79
|
+
}
|
|
80
|
+
// subscriberId (string)
|
|
81
|
+
if (resource.subscriberId !== undefined && typeof resource.subscriberId !== "string") {
|
|
82
|
+
issues.push({
|
|
83
|
+
severity: "error",
|
|
84
|
+
path: `${path}.subscriberId`,
|
|
85
|
+
code: "invalid-type",
|
|
86
|
+
message: `Expected '${path}.subscriberId' to be a string.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// type (CodeableConcept)
|
|
90
|
+
if (resource.type !== undefined) {
|
|
91
|
+
issues.push(...validateCodeableConcept(resource.type, `${path}.type`));
|
|
92
|
+
}
|
|
93
|
+
// relationship (CodeableConcept)
|
|
94
|
+
if (resource.relationship !== undefined) {
|
|
95
|
+
issues.push(...validateCodeableConcept(resource.relationship, `${path}.relationship`));
|
|
96
|
+
}
|
|
97
|
+
// period (Period)
|
|
98
|
+
if (resource.period !== undefined) {
|
|
99
|
+
if (typeof resource.period !== "object" || resource.period === null || Array.isArray(resource.period)) {
|
|
100
|
+
issues.push({
|
|
101
|
+
severity: "error",
|
|
102
|
+
path: `${path}.period`,
|
|
103
|
+
code: "invalid-structure",
|
|
104
|
+
message: `Expected '${path}.period' to be an object.`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return issues;
|
|
109
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { validateCodeableConcept, validateCoding } from "../coding.js";
|
|
2
|
+
import { validateReference } from "../reference-rules.js";
|
|
3
|
+
const VALID_ENCOUNTER_STATUSES = [
|
|
4
|
+
"planned",
|
|
5
|
+
"arrived",
|
|
6
|
+
"triaged",
|
|
7
|
+
"in-progress",
|
|
8
|
+
"onleave",
|
|
9
|
+
"finished",
|
|
10
|
+
"cancelled",
|
|
11
|
+
"entered-in-error",
|
|
12
|
+
"unknown",
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Validates base structural rules for FHIR R4 Encounter resource.
|
|
16
|
+
*/
|
|
17
|
+
export function validateEncounter(resource, path = "Encounter") {
|
|
18
|
+
const issues = [];
|
|
19
|
+
if (resource.resourceType !== "Encounter") {
|
|
20
|
+
issues.push({
|
|
21
|
+
severity: "error",
|
|
22
|
+
path: `${path}.resourceType`,
|
|
23
|
+
code: "invalid-resource-type",
|
|
24
|
+
message: `Expected resourceType 'Encounter', found '${String(resource.resourceType)}'.`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// status (Required)
|
|
28
|
+
if (resource.status === undefined || resource.status === null || resource.status === "") {
|
|
29
|
+
issues.push({
|
|
30
|
+
severity: "error",
|
|
31
|
+
path: `${path}.status`,
|
|
32
|
+
code: "required",
|
|
33
|
+
message: `Missing required 'status' in '${path}'.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (typeof resource.status !== "string" || !VALID_ENCOUNTER_STATUSES.includes(resource.status)) {
|
|
37
|
+
issues.push({
|
|
38
|
+
severity: "error",
|
|
39
|
+
path: `${path}.status`,
|
|
40
|
+
code: "invalid-value",
|
|
41
|
+
message: `Invalid Encounter status '${String(resource.status)}'. Allowed values: ${VALID_ENCOUNTER_STATUSES.join(", ")}.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// class (Required Coding / Coding-like object)
|
|
45
|
+
if (resource.class === undefined || resource.class === null) {
|
|
46
|
+
issues.push({
|
|
47
|
+
severity: "error",
|
|
48
|
+
path: `${path}.class`,
|
|
49
|
+
code: "required",
|
|
50
|
+
message: `Missing required 'class' in '${path}'.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// In FHIR R4, Encounter.class is a Coding
|
|
55
|
+
issues.push(...validateCoding(resource.class, `${path}.class`));
|
|
56
|
+
}
|
|
57
|
+
// subject (Reference: Patient | Group)
|
|
58
|
+
if (resource.subject !== undefined) {
|
|
59
|
+
issues.push(...validateReference(resource.subject, `${path}.subject`, ["Patient", "Group"]));
|
|
60
|
+
}
|
|
61
|
+
// period (Period)
|
|
62
|
+
if (resource.period !== undefined) {
|
|
63
|
+
if (typeof resource.period !== "object" || resource.period === null || Array.isArray(resource.period)) {
|
|
64
|
+
issues.push({
|
|
65
|
+
severity: "error",
|
|
66
|
+
path: `${path}.period`,
|
|
67
|
+
code: "invalid-structure",
|
|
68
|
+
message: `Expected '${path}.period' to be an object.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const p = resource.period;
|
|
73
|
+
if (p.start !== undefined && typeof p.start !== "string") {
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "error",
|
|
76
|
+
path: `${path}.period.start`,
|
|
77
|
+
code: "invalid-type",
|
|
78
|
+
message: `Expected '${path}.period.start' to be a string.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (p.end !== undefined && typeof p.end !== "string") {
|
|
82
|
+
issues.push({
|
|
83
|
+
severity: "error",
|
|
84
|
+
path: `${path}.period.end`,
|
|
85
|
+
code: "invalid-type",
|
|
86
|
+
message: `Expected '${path}.period.end' to be a string.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// participant
|
|
92
|
+
if (resource.participant !== undefined) {
|
|
93
|
+
if (!Array.isArray(resource.participant)) {
|
|
94
|
+
issues.push({
|
|
95
|
+
severity: "error",
|
|
96
|
+
path: `${path}.participant`,
|
|
97
|
+
code: "invalid-type",
|
|
98
|
+
message: `Expected '${path}.participant' to be an array.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
resource.participant.forEach((part, idx) => {
|
|
103
|
+
const partPath = `${path}.participant[${idx}]`;
|
|
104
|
+
if (typeof part !== "object" || part === null || Array.isArray(part)) {
|
|
105
|
+
issues.push({
|
|
106
|
+
severity: "error",
|
|
107
|
+
path: partPath,
|
|
108
|
+
code: "invalid-structure",
|
|
109
|
+
message: `Expected '${partPath}' to be an object.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const p = part;
|
|
114
|
+
if (p.individual !== undefined) {
|
|
115
|
+
issues.push(...validateReference(p.individual, `${partPath}.individual`, ["Practitioner", "PractitionerRole", "RelatedPerson"]));
|
|
116
|
+
}
|
|
117
|
+
if (p.type !== undefined && Array.isArray(p.type)) {
|
|
118
|
+
p.type.forEach((t, tIdx) => {
|
|
119
|
+
issues.push(...validateCodeableConcept(t, `${partPath}.type[${tIdx}]`));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// serviceProvider (Reference: Organization)
|
|
127
|
+
if (resource.serviceProvider !== undefined) {
|
|
128
|
+
issues.push(...validateReference(resource.serviceProvider, `${path}.serviceProvider`, ["Organization"]));
|
|
129
|
+
}
|
|
130
|
+
// type (CodeableConcept[])
|
|
131
|
+
if (resource.type !== undefined && Array.isArray(resource.type)) {
|
|
132
|
+
resource.type.forEach((t, idx) => {
|
|
133
|
+
issues.push(...validateCodeableConcept(t, `${path}.type[${idx}]`));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return issues;
|
|
137
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { validateCodeableConcept } from "../coding.js";
|
|
2
|
+
import { validateReference } from "../reference-rules.js";
|
|
3
|
+
const VALID_OBSERVATION_STATUSES = [
|
|
4
|
+
"registered",
|
|
5
|
+
"preliminary",
|
|
6
|
+
"final",
|
|
7
|
+
"amended",
|
|
8
|
+
"corrected",
|
|
9
|
+
"cancelled",
|
|
10
|
+
"entered-in-error",
|
|
11
|
+
"unknown",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Validates base structural rules for FHIR R4 Observation resource.
|
|
15
|
+
*/
|
|
16
|
+
export function validateObservation(resource, path = "Observation") {
|
|
17
|
+
const issues = [];
|
|
18
|
+
const effectiveFields = ["effectiveDateTime", "effectivePeriod", "effectiveTiming", "effectiveInstant"].filter((field) => resource[field] !== undefined);
|
|
19
|
+
if (effectiveFields.length > 1)
|
|
20
|
+
issues.push({ severity: "error", path: `${path}.effective[x]`, code: "cardinality", message: "Observation may contain only one effective[x] value." });
|
|
21
|
+
const valueFields = ["valueQuantity", "valueCodeableConcept", "valueString", "valueBoolean", "valueInteger", "valueRange", "valueRatio", "valueSampledData", "valueTime", "valueDateTime", "valuePeriod"].filter((field) => resource[field] !== undefined);
|
|
22
|
+
if (valueFields.length > 1)
|
|
23
|
+
issues.push({ severity: "error", path: `${path}.value[x]`, code: "cardinality", message: "Observation may contain only one value[x] value." });
|
|
24
|
+
if (resource.resourceType !== "Observation") {
|
|
25
|
+
issues.push({
|
|
26
|
+
severity: "error",
|
|
27
|
+
path: `${path}.resourceType`,
|
|
28
|
+
code: "invalid-resource-type",
|
|
29
|
+
message: `Expected resourceType 'Observation', found '${String(resource.resourceType)}'.`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// status (Required)
|
|
33
|
+
if (resource.status === undefined || resource.status === null || resource.status === "") {
|
|
34
|
+
issues.push({
|
|
35
|
+
severity: "error",
|
|
36
|
+
path: `${path}.status`,
|
|
37
|
+
code: "required",
|
|
38
|
+
message: `Missing required 'status' in '${path}'.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (typeof resource.status !== "string" || !VALID_OBSERVATION_STATUSES.includes(resource.status)) {
|
|
42
|
+
issues.push({
|
|
43
|
+
severity: "error",
|
|
44
|
+
path: `${path}.status`,
|
|
45
|
+
code: "invalid-value",
|
|
46
|
+
message: `Invalid Observation status '${String(resource.status)}'. Allowed values: ${VALID_OBSERVATION_STATUSES.join(", ")}.`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// code (Required CodeableConcept)
|
|
50
|
+
if (resource.code === undefined || resource.code === null) {
|
|
51
|
+
issues.push({
|
|
52
|
+
severity: "error",
|
|
53
|
+
path: `${path}.code`,
|
|
54
|
+
code: "required",
|
|
55
|
+
message: `Missing required 'code' in '${path}'.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
issues.push(...validateCodeableConcept(resource.code, `${path}.code`, { requireCoding: true }));
|
|
60
|
+
}
|
|
61
|
+
// category (CodeableConcept[])
|
|
62
|
+
if (resource.category !== undefined) {
|
|
63
|
+
if (!Array.isArray(resource.category)) {
|
|
64
|
+
issues.push({
|
|
65
|
+
severity: "error",
|
|
66
|
+
path: `${path}.category`,
|
|
67
|
+
code: "invalid-type",
|
|
68
|
+
message: `Expected '${path}.category' to be an array, got ${typeof resource.category}.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resource.category.forEach((cat, idx) => {
|
|
73
|
+
issues.push(...validateCodeableConcept(cat, `${path}.category[${idx}]`));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// subject (Reference: Patient | Group | Device | Location)
|
|
78
|
+
if (resource.subject !== undefined) {
|
|
79
|
+
issues.push(...validateReference(resource.subject, `${path}.subject`, ["Patient", "Group", "Device", "Location"]));
|
|
80
|
+
}
|
|
81
|
+
// encounter (Reference: Encounter)
|
|
82
|
+
if (resource.encounter !== undefined) {
|
|
83
|
+
issues.push(...validateReference(resource.encounter, `${path}.encounter`, ["Encounter"]));
|
|
84
|
+
}
|
|
85
|
+
// performer (Reference[])
|
|
86
|
+
if (resource.performer !== undefined) {
|
|
87
|
+
if (!Array.isArray(resource.performer)) {
|
|
88
|
+
issues.push({
|
|
89
|
+
severity: "error",
|
|
90
|
+
path: `${path}.performer`,
|
|
91
|
+
code: "invalid-type",
|
|
92
|
+
message: `Expected '${path}.performer' to be an array, got ${typeof resource.performer}.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
resource.performer.forEach((perf, idx) => {
|
|
97
|
+
issues.push(...validateReference(perf, `${path}.performer[${idx}]`, ["Practitioner", "PractitionerRole", "Organization", "CareTeam", "Patient", "RelatedPerson"]));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// valueQuantity
|
|
102
|
+
if (resource.valueQuantity !== undefined) {
|
|
103
|
+
if (typeof resource.valueQuantity !== "object" || resource.valueQuantity === null || Array.isArray(resource.valueQuantity)) {
|
|
104
|
+
issues.push({
|
|
105
|
+
severity: "error",
|
|
106
|
+
path: `${path}.valueQuantity`,
|
|
107
|
+
code: "invalid-structure",
|
|
108
|
+
message: `Expected '${path}.valueQuantity' to be an object.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const q = resource.valueQuantity;
|
|
113
|
+
if (q.value !== undefined && (typeof q.value !== "number" || !Number.isFinite(q.value))) {
|
|
114
|
+
issues.push({
|
|
115
|
+
severity: "error",
|
|
116
|
+
path: `${path}.valueQuantity.value`,
|
|
117
|
+
code: "invalid-type",
|
|
118
|
+
message: `Expected '${path}.valueQuantity.value' to be a number, got ${typeof q.value}.`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (q.unit !== undefined && typeof q.unit !== "string") {
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "error",
|
|
124
|
+
path: `${path}.valueQuantity.unit`,
|
|
125
|
+
code: "invalid-type",
|
|
126
|
+
message: `Expected '${path}.valueQuantity.unit' to be a string.`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (q.system !== undefined && typeof q.system !== "string") {
|
|
130
|
+
issues.push({
|
|
131
|
+
severity: "error",
|
|
132
|
+
path: `${path}.valueQuantity.system`,
|
|
133
|
+
code: "invalid-type",
|
|
134
|
+
message: `Expected '${path}.valueQuantity.system' to be a string.`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (q.code !== undefined && typeof q.code !== "string") {
|
|
138
|
+
issues.push({
|
|
139
|
+
severity: "error",
|
|
140
|
+
path: `${path}.valueQuantity.code`,
|
|
141
|
+
code: "invalid-type",
|
|
142
|
+
message: `Expected '${path}.valueQuantity.code' to be a string.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (q.system !== undefined && q.system === "http://unitsofmeasure.org" && (typeof q.code !== "string" || !q.code.trim())) {
|
|
146
|
+
issues.push({ severity: "error", path: `${path}.valueQuantity.code`, code: "required", message: "UCUM Quantity requires a non-empty code." });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (resource.effectiveDateTime !== undefined && (typeof resource.effectiveDateTime !== "string" || Number.isNaN(Date.parse(resource.effectiveDateTime)))) {
|
|
151
|
+
issues.push({ severity: "error", path: `${path}.effectiveDateTime`, code: "invalid-value", message: "effectiveDateTime must be a valid date-time string." });
|
|
152
|
+
}
|
|
153
|
+
if (resource.effectivePeriod !== undefined) {
|
|
154
|
+
if (!resource.effectivePeriod || typeof resource.effectivePeriod !== "object" || Array.isArray(resource.effectivePeriod)) {
|
|
155
|
+
issues.push({ severity: "error", path: `${path}.effectivePeriod`, code: "invalid-structure", message: "effectivePeriod must be an object." });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const period = resource.effectivePeriod;
|
|
159
|
+
const start = typeof period.start === "string" ? Date.parse(period.start) : NaN;
|
|
160
|
+
const end = typeof period.end === "string" ? Date.parse(period.end) : NaN;
|
|
161
|
+
if (Number.isNaN(start) || Number.isNaN(end) || start > end)
|
|
162
|
+
issues.push({ severity: "error", path: `${path}.effectivePeriod`, code: "invalid-value", message: "effectivePeriod requires valid start and end values in chronological order." });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// valueCodeableConcept
|
|
166
|
+
if (resource.valueCodeableConcept !== undefined) {
|
|
167
|
+
issues.push(...validateCodeableConcept(resource.valueCodeableConcept, `${path}.valueCodeableConcept`));
|
|
168
|
+
}
|
|
169
|
+
// component (Array of components)
|
|
170
|
+
if (resource.component !== undefined) {
|
|
171
|
+
if (!Array.isArray(resource.component)) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: "error",
|
|
174
|
+
path: `${path}.component`,
|
|
175
|
+
code: "invalid-type",
|
|
176
|
+
message: `Expected '${path}.component' to be an array, got ${typeof resource.component}.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
resource.component.forEach((comp, idx) => {
|
|
181
|
+
const compPath = `${path}.component[${idx}]`;
|
|
182
|
+
if (typeof comp !== "object" || comp === null || Array.isArray(comp)) {
|
|
183
|
+
issues.push({
|
|
184
|
+
severity: "error",
|
|
185
|
+
path: compPath,
|
|
186
|
+
code: "invalid-structure",
|
|
187
|
+
message: `Expected '${compPath}' to be an object.`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const c = comp;
|
|
192
|
+
if (c.code === undefined || c.code === null) {
|
|
193
|
+
issues.push({
|
|
194
|
+
severity: "error",
|
|
195
|
+
path: `${compPath}.code`,
|
|
196
|
+
code: "required",
|
|
197
|
+
message: `Missing required 'code' in '${compPath}'.`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
issues.push(...validateCodeableConcept(c.code, `${compPath}.code`, { requireCoding: true }));
|
|
202
|
+
}
|
|
203
|
+
if (c.valueQuantity !== undefined) {
|
|
204
|
+
const q = c.valueQuantity;
|
|
205
|
+
if (!q || Array.isArray(q)) {
|
|
206
|
+
issues.push({ severity: "error", path: `${compPath}.valueQuantity`, code: "invalid-structure", message: `Expected '${compPath}.valueQuantity' to be an object.` });
|
|
207
|
+
}
|
|
208
|
+
else if (typeof q.value !== "number" || !Number.isFinite(q.value)) {
|
|
209
|
+
issues.push({
|
|
210
|
+
severity: "error",
|
|
211
|
+
path: `${compPath}.valueQuantity.value`,
|
|
212
|
+
code: "invalid-type",
|
|
213
|
+
message: `Expected '${compPath}.valueQuantity.value' to be a number.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (c.valueCodeableConcept !== undefined) {
|
|
218
|
+
issues.push(...validateCodeableConcept(c.valueCodeableConcept, `${compPath}.valueCodeableConcept`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// hasMember / derivedFrom
|
|
225
|
+
if (resource.hasMember !== undefined) {
|
|
226
|
+
if (Array.isArray(resource.hasMember)) {
|
|
227
|
+
resource.hasMember.forEach((hm, idx) => {
|
|
228
|
+
issues.push(...validateReference(hm, `${path}.hasMember[${idx}]`, ["Observation", "QuestionnaireResponse", "MolecularSequence"]));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (resource.derivedFrom !== undefined) {
|
|
233
|
+
if (Array.isArray(resource.derivedFrom)) {
|
|
234
|
+
resource.derivedFrom.forEach((df, idx) => {
|
|
235
|
+
issues.push(...validateReference(df, `${path}.derivedFrom[${idx}]`, ["DocumentReference", "ImagingStudy", "Media", "QuestionnaireResponse", "Observation", "MolecularSequence"]));
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { validateCodeableConcept } from "../coding.js";
|
|
2
|
+
import { validateReference } from "../reference-rules.js";
|
|
3
|
+
const VALID_GENDERS = ["male", "female", "other", "unknown"];
|
|
4
|
+
const DATE_REGEX = /^\d{4}(-\d{2}(-\d{2})?)?$/;
|
|
5
|
+
/**
|
|
6
|
+
* Validates base structural rules for FHIR R4 Patient resource.
|
|
7
|
+
*/
|
|
8
|
+
export function validatePatient(resource, path = "Patient") {
|
|
9
|
+
const issues = [];
|
|
10
|
+
if (resource.resourceType !== "Patient") {
|
|
11
|
+
issues.push({
|
|
12
|
+
severity: "error",
|
|
13
|
+
path: `${path}.resourceType`,
|
|
14
|
+
code: "invalid-resource-type",
|
|
15
|
+
message: `Expected resourceType 'Patient', found '${String(resource.resourceType)}'.`,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// active
|
|
19
|
+
if (resource.active !== undefined && typeof resource.active !== "boolean") {
|
|
20
|
+
issues.push({
|
|
21
|
+
severity: "error",
|
|
22
|
+
path: `${path}.active`,
|
|
23
|
+
code: "invalid-type",
|
|
24
|
+
message: `Expected '${path}.active' to be a boolean, got ${typeof resource.active}.`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// gender
|
|
28
|
+
if (resource.gender !== undefined) {
|
|
29
|
+
if (typeof resource.gender !== "string" || !VALID_GENDERS.includes(resource.gender)) {
|
|
30
|
+
issues.push({
|
|
31
|
+
severity: "error",
|
|
32
|
+
path: `${path}.gender`,
|
|
33
|
+
code: "invalid-value",
|
|
34
|
+
message: `Invalid gender '${String(resource.gender)}'. Allowed values: ${VALID_GENDERS.join(", ")}.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// birthDate
|
|
39
|
+
if (resource.birthDate !== undefined) {
|
|
40
|
+
if (typeof resource.birthDate !== "string" || !DATE_REGEX.test(resource.birthDate)) {
|
|
41
|
+
issues.push({
|
|
42
|
+
severity: "error",
|
|
43
|
+
path: `${path}.birthDate`,
|
|
44
|
+
code: "invalid-format",
|
|
45
|
+
message: `Expected '${path}.birthDate' to match YYYY, YYYY-MM, or YYYY-MM-DD format.`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// name (HumanName[])
|
|
50
|
+
if (resource.name !== undefined) {
|
|
51
|
+
if (!Array.isArray(resource.name)) {
|
|
52
|
+
issues.push({
|
|
53
|
+
severity: "error",
|
|
54
|
+
path: `${path}.name`,
|
|
55
|
+
code: "invalid-type",
|
|
56
|
+
message: `Expected '${path}.name' to be an array, got ${typeof resource.name}.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
resource.name.forEach((n, idx) => {
|
|
61
|
+
const namePath = `${path}.name[${idx}]`;
|
|
62
|
+
if (typeof n !== "object" || n === null) {
|
|
63
|
+
issues.push({
|
|
64
|
+
severity: "error",
|
|
65
|
+
path: namePath,
|
|
66
|
+
code: "invalid-structure",
|
|
67
|
+
message: `Expected '${namePath}' to be a HumanName object.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const hn = n;
|
|
72
|
+
if (hn.family !== undefined && typeof hn.family !== "string") {
|
|
73
|
+
issues.push({
|
|
74
|
+
severity: "error",
|
|
75
|
+
path: `${namePath}.family`,
|
|
76
|
+
code: "invalid-type",
|
|
77
|
+
message: `Expected '${namePath}.family' to be a string.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (hn.given !== undefined && (!Array.isArray(hn.given) || hn.given.some((g) => typeof g !== "string"))) {
|
|
81
|
+
issues.push({
|
|
82
|
+
severity: "error",
|
|
83
|
+
path: `${namePath}.given`,
|
|
84
|
+
code: "invalid-type",
|
|
85
|
+
message: `Expected '${namePath}.given' to be an array of strings.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// identifier (Identifier[])
|
|
93
|
+
if (resource.identifier !== undefined) {
|
|
94
|
+
if (!Array.isArray(resource.identifier)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
severity: "error",
|
|
97
|
+
path: `${path}.identifier`,
|
|
98
|
+
code: "invalid-type",
|
|
99
|
+
message: `Expected '${path}.identifier' to be an array, got ${typeof resource.identifier}.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
resource.identifier.forEach((ident, idx) => {
|
|
104
|
+
const idPath = `${path}.identifier[${idx}]`;
|
|
105
|
+
if (typeof ident !== "object" || ident === null) {
|
|
106
|
+
issues.push({
|
|
107
|
+
severity: "error",
|
|
108
|
+
path: idPath,
|
|
109
|
+
code: "invalid-structure",
|
|
110
|
+
message: `Expected '${idPath}' to be an Identifier object.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const idObj = ident;
|
|
115
|
+
if (idObj.system !== undefined && typeof idObj.system !== "string") {
|
|
116
|
+
issues.push({
|
|
117
|
+
severity: "error",
|
|
118
|
+
path: `${idPath}.system`,
|
|
119
|
+
code: "invalid-type",
|
|
120
|
+
message: `Expected '${idPath}.system' to be a string.`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (idObj.value !== undefined && typeof idObj.value !== "string") {
|
|
124
|
+
issues.push({
|
|
125
|
+
severity: "error",
|
|
126
|
+
path: `${idPath}.value`,
|
|
127
|
+
code: "invalid-type",
|
|
128
|
+
message: `Expected '${idPath}.value' to be a string.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (idObj.type !== undefined) {
|
|
132
|
+
issues.push(...validateCodeableConcept(idObj.type, `${idPath}.type`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// generalPractitioner (Reference[])
|
|
139
|
+
if (resource.generalPractitioner !== undefined) {
|
|
140
|
+
if (!Array.isArray(resource.generalPractitioner)) {
|
|
141
|
+
issues.push({
|
|
142
|
+
severity: "error",
|
|
143
|
+
path: `${path}.generalPractitioner`,
|
|
144
|
+
code: "invalid-type",
|
|
145
|
+
message: `Expected '${path}.generalPractitioner' to be an array.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
resource.generalPractitioner.forEach((gp, idx) => {
|
|
150
|
+
issues.push(...validateReference(gp, `${path}.generalPractitioner[${idx}]`, ["Organization", "Practitioner", "PractitionerRole"]));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// managingOrganization (Reference)
|
|
155
|
+
if (resource.managingOrganization !== undefined) {
|
|
156
|
+
issues.push(...validateReference(resource.managingOrganization, `${path}.managingOrganization`, ["Organization"]));
|
|
157
|
+
}
|
|
158
|
+
// maritalStatus (CodeableConcept)
|
|
159
|
+
if (resource.maritalStatus !== undefined) {
|
|
160
|
+
issues.push(...validateCodeableConcept(resource.maritalStatus, `${path}.maritalStatus`));
|
|
161
|
+
}
|
|
162
|
+
return issues;
|
|
163
|
+
}
|