@peerbits/fhir-validator 1.0.0 → 1.0.1
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 +3 -1
- 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 +213 -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 +201 -0
- package/package.json +3 -2
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fhir-validator — structural, cardinality, and reference validation for
|
|
3
|
+
* FHIR R4 resources. Explicitly NOT a full conformance engine — see
|
|
4
|
+
* docs/KNOWN_LIMITATIONS.md and developer handover spec.
|
|
5
|
+
*/
|
|
6
|
+
export declare const VERSION = "1.0.1";
|
|
7
|
+
export { validate, getNestedValue, validateProfileConstraints, hasCircularReference } from "./validate.js";
|
|
8
|
+
export type { ValidationIssue, ValidationResult, ValidationSeverity, ProfileConstraints, BindingStrength, ValidateOptions, } from "./types.js";
|
|
9
|
+
export { validateCoding, validateCodeableConcept } from "./coding.js";
|
|
10
|
+
export { validateReference, extractReferenceType, ALLOWED_REFERENCE_TARGETS } from "./reference-rules.js";
|
|
11
|
+
export { validatePatient } from "./base-definitions/patient.js";
|
|
12
|
+
export { validateObservation } from "./base-definitions/observation.js";
|
|
13
|
+
export { validateEncounter } from "./base-definitions/encounter.js";
|
|
14
|
+
export { validateCondition } from "./base-definitions/condition.js";
|
|
15
|
+
export { validateCoverage } from "./base-definitions/coverage.js";
|
|
16
|
+
export { validateClaim } from "./base-definitions/claim.js";
|
|
17
|
+
export { validateClaimResponse } from "./base-definitions/claim-response.js";
|
|
18
|
+
export { USCorePatientProfile } from "./profiles/us-core-patient.js";
|
|
19
|
+
export { USCoreObservationVitalsProfile } from "./profiles/us-core-observation-vitals.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fhir-validator — structural, cardinality, and reference validation for
|
|
3
|
+
* FHIR R4 resources. Explicitly NOT a full conformance engine — see
|
|
4
|
+
* docs/KNOWN_LIMITATIONS.md and developer handover spec.
|
|
5
|
+
*/
|
|
6
|
+
export const VERSION = "1.0.1";
|
|
7
|
+
// Main API
|
|
8
|
+
export { validate, getNestedValue, validateProfileConstraints, hasCircularReference } from "./validate.js";
|
|
9
|
+
// Coding & Reference Helpers
|
|
10
|
+
export { validateCoding, validateCodeableConcept } from "./coding.js";
|
|
11
|
+
export { validateReference, extractReferenceType, ALLOWED_REFERENCE_TARGETS } from "./reference-rules.js";
|
|
12
|
+
// Base Resource Validators
|
|
13
|
+
export { validatePatient } from "./base-definitions/patient.js";
|
|
14
|
+
export { validateObservation } from "./base-definitions/observation.js";
|
|
15
|
+
export { validateEncounter } from "./base-definitions/encounter.js";
|
|
16
|
+
export { validateCondition } from "./base-definitions/condition.js";
|
|
17
|
+
export { validateCoverage } from "./base-definitions/coverage.js";
|
|
18
|
+
export { validateClaim } from "./base-definitions/claim.js";
|
|
19
|
+
export { validateClaimResponse } from "./base-definitions/claim-response.js";
|
|
20
|
+
// Illustrative Profiles
|
|
21
|
+
export { USCorePatientProfile } from "./profiles/us-core-patient.js";
|
|
22
|
+
export { USCoreObservationVitalsProfile } from "./profiles/us-core-observation-vitals.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ProfileConstraints } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* ILLUSTRATIVE ONLY — not a certified reproduction of the real US Core
|
|
4
|
+
* Vital Signs profile. See handover doc §6 FR5 and §13.
|
|
5
|
+
*
|
|
6
|
+
* This profile demonstrates the ProfileConstraints mechanism by enforcing:
|
|
7
|
+
* - Presence of category containing vital-signs coding
|
|
8
|
+
* - Mandatory subject referencing Patient
|
|
9
|
+
* - Effective date/time or period
|
|
10
|
+
*/
|
|
11
|
+
export declare const USCoreObservationVitalsProfile: ProfileConstraints;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ILLUSTRATIVE ONLY — not a certified reproduction of the real US Core
|
|
3
|
+
* Vital Signs profile. See handover doc §6 FR5 and §13.
|
|
4
|
+
*
|
|
5
|
+
* This profile demonstrates the ProfileConstraints mechanism by enforcing:
|
|
6
|
+
* - Presence of category containing vital-signs coding
|
|
7
|
+
* - Mandatory subject referencing Patient
|
|
8
|
+
* - Effective date/time or period
|
|
9
|
+
*/
|
|
10
|
+
export const USCoreObservationVitalsProfile = {
|
|
11
|
+
name: "us-core-observation-vitals",
|
|
12
|
+
resourceType: "Observation",
|
|
13
|
+
url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs",
|
|
14
|
+
requiredElements: ["status", "category", "code", "subject"],
|
|
15
|
+
cardinalityOverrides: {
|
|
16
|
+
category: { min: 1 },
|
|
17
|
+
},
|
|
18
|
+
fixedValues: {
|
|
19
|
+
"category[0].coding[0].system": "http://terminology.hl7.org/CodeSystem/observation-category",
|
|
20
|
+
"category[0].coding[0].code": "vital-signs",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ProfileConstraints } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* ILLUSTRATIVE ONLY — not a certified reproduction of the real US Core
|
|
4
|
+
* Patient StructureDefinition. See handover doc §6 FR5 and §13.
|
|
5
|
+
*
|
|
6
|
+
* This profile demonstrates the ProfileConstraints mechanism by requiring
|
|
7
|
+
* elements (such as `identifier`, `name`, `gender`) that the base FHIR R4
|
|
8
|
+
* specification leaves optional.
|
|
9
|
+
*/
|
|
10
|
+
export declare const USCorePatientProfile: ProfileConstraints;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ILLUSTRATIVE ONLY — not a certified reproduction of the real US Core
|
|
3
|
+
* Patient StructureDefinition. See handover doc §6 FR5 and §13.
|
|
4
|
+
*
|
|
5
|
+
* This profile demonstrates the ProfileConstraints mechanism by requiring
|
|
6
|
+
* elements (such as `identifier`, `name`, `gender`) that the base FHIR R4
|
|
7
|
+
* specification leaves optional.
|
|
8
|
+
*/
|
|
9
|
+
export const USCorePatientProfile = {
|
|
10
|
+
name: "us-core-patient",
|
|
11
|
+
resourceType: "Patient",
|
|
12
|
+
url: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
|
|
13
|
+
requiredElements: ["identifier", "name", "gender"],
|
|
14
|
+
cardinalityOverrides: {
|
|
15
|
+
identifier: { min: 1 },
|
|
16
|
+
name: { min: 1 },
|
|
17
|
+
},
|
|
18
|
+
bindingStrength: {
|
|
19
|
+
gender: "required",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ValidationIssue } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps field paths to their allowed target FHIR resource types per FHIR R4 base spec.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ALLOWED_REFERENCE_TARGETS: Record<string, string[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Extracts target resource type from a Reference object.
|
|
8
|
+
* Looks at explicit `type` field, or parses relative/absolute URL in `reference`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractReferenceType(referenceObj: Record<string, unknown>): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Validates a FHIR Reference element against allowed target resource types.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateReference(reference: unknown, path: string, allowedTypes?: string[]): ValidationIssue[];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps field paths to their allowed target FHIR resource types per FHIR R4 base spec.
|
|
3
|
+
*/
|
|
4
|
+
export const ALLOWED_REFERENCE_TARGETS = {
|
|
5
|
+
// Patient
|
|
6
|
+
"Patient.generalPractitioner": ["Organization", "Practitioner", "PractitionerRole"],
|
|
7
|
+
"Patient.managingOrganization": ["Organization"],
|
|
8
|
+
"Patient.link.other": ["Patient", "RelatedPerson"],
|
|
9
|
+
// Observation
|
|
10
|
+
"Observation.subject": ["Patient", "Group", "Device", "Location"],
|
|
11
|
+
"Observation.focus": ["Resource"],
|
|
12
|
+
"Observation.encounter": ["Encounter"],
|
|
13
|
+
"Observation.performer": ["Practitioner", "PractitionerRole", "Organization", "CareTeam", "Patient", "RelatedPerson"],
|
|
14
|
+
"Observation.specimen": ["Specimen"],
|
|
15
|
+
"Observation.device": ["Device", "DeviceMetric"],
|
|
16
|
+
"Observation.hasMember": ["Observation", "QuestionnaireResponse", "MolecularSequence"],
|
|
17
|
+
"Observation.derivedFrom": ["DocumentReference", "ImagingStudy", "Media", "QuestionnaireResponse", "Observation", "MolecularSequence"],
|
|
18
|
+
// Encounter
|
|
19
|
+
"Encounter.subject": ["Patient", "Group"],
|
|
20
|
+
"Encounter.episodeOfCare": ["EpisodeOfCare"],
|
|
21
|
+
"Encounter.basedOn": ["ServiceRequest"],
|
|
22
|
+
"Encounter.participant.individual": ["Practitioner", "PractitionerRole", "RelatedPerson"],
|
|
23
|
+
"Encounter.appointment": ["Appointment"],
|
|
24
|
+
"Encounter.reasonReference": ["Condition", "Procedure", "Observation", "ImmunizationRecommendation"],
|
|
25
|
+
"Encounter.diagnosis.condition": ["Condition", "Procedure"],
|
|
26
|
+
"Encounter.location.location": ["Location"],
|
|
27
|
+
"Encounter.serviceProvider": ["Organization"],
|
|
28
|
+
"Encounter.partOf": ["Encounter"],
|
|
29
|
+
// Condition
|
|
30
|
+
"Condition.subject": ["Patient", "Group"],
|
|
31
|
+
"Condition.encounter": ["Encounter"],
|
|
32
|
+
"Condition.recorder": ["Practitioner", "PractitionerRole", "Patient", "RelatedPerson"],
|
|
33
|
+
"Condition.asserter": ["Practitioner", "PractitionerRole", "Patient", "RelatedPerson"],
|
|
34
|
+
"Condition.stage.assessment": ["ClinicalImpression", "DiagnosticReport", "Observation"],
|
|
35
|
+
"Condition.evidence.detail": ["Resource"],
|
|
36
|
+
// Coverage
|
|
37
|
+
"Coverage.policyHolder": ["Patient", "RelatedPerson", "Organization"],
|
|
38
|
+
"Coverage.subscriber": ["Patient", "RelatedPerson"],
|
|
39
|
+
"Coverage.beneficiary": ["Patient"],
|
|
40
|
+
"Coverage.payor": ["Organization", "Patient", "RelatedPerson"],
|
|
41
|
+
"Coverage.contract": ["Contract"],
|
|
42
|
+
// Claim
|
|
43
|
+
"Claim.patient": ["Patient"],
|
|
44
|
+
"Claim.enterer": ["Practitioner", "PractitionerRole"],
|
|
45
|
+
"Claim.insurer": ["Organization"],
|
|
46
|
+
"Claim.provider": ["Practitioner", "PractitionerRole", "Organization"],
|
|
47
|
+
"Claim.facility": ["Location"],
|
|
48
|
+
"Claim.prescription": ["MedicationRequest", "VisionPrescription"],
|
|
49
|
+
"Claim.originalPrescription": ["MedicationRequest"],
|
|
50
|
+
"Claim.payee.party": ["Practitioner", "PractitionerRole", "Organization", "Patient", "RelatedPerson"],
|
|
51
|
+
"Claim.referral": ["ServiceRequest"],
|
|
52
|
+
"Claim.careTeam.provider": ["Practitioner", "PractitionerRole", "Organization"],
|
|
53
|
+
"Claim.supportingInfo.valueReference": ["Resource"],
|
|
54
|
+
"Claim.diagnosis.diagnosisReference": ["Condition"],
|
|
55
|
+
"Claim.procedure.procedureReference": ["Procedure"],
|
|
56
|
+
"Claim.insurance.coverage": ["Coverage"],
|
|
57
|
+
"Claim.insurance.claimResponse": ["ClaimResponse"],
|
|
58
|
+
"Claim.item.locationReference": ["Location"],
|
|
59
|
+
"Claim.item.encounter": ["Encounter"],
|
|
60
|
+
// ClaimResponse
|
|
61
|
+
"ClaimResponse.patient": ["Patient"],
|
|
62
|
+
"ClaimResponse.insurer": ["Organization"],
|
|
63
|
+
"ClaimResponse.requestor": ["Practitioner", "PractitionerRole", "Organization"],
|
|
64
|
+
"ClaimResponse.request": ["Claim"],
|
|
65
|
+
"ClaimResponse.insurance.coverage": ["Coverage"],
|
|
66
|
+
"ClaimResponse.insurance.claimResponse": ["ClaimResponse"],
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Extracts target resource type from a Reference object.
|
|
70
|
+
* Looks at explicit `type` field, or parses relative/absolute URL in `reference`.
|
|
71
|
+
*/
|
|
72
|
+
export function extractReferenceType(referenceObj) {
|
|
73
|
+
if (typeof referenceObj.type === "string" && referenceObj.type.trim()) {
|
|
74
|
+
return referenceObj.type.trim();
|
|
75
|
+
}
|
|
76
|
+
if (typeof referenceObj.reference === "string") {
|
|
77
|
+
const ref = referenceObj.reference.trim();
|
|
78
|
+
// Pattern: [ResourceType]/[id] or http[s]://.../[ResourceType]/[id]
|
|
79
|
+
const match = ref.match(/(?:^|\/)([A-Z][A-Za-z0-9]+)\/[A-Za-z0-9\-._~]+$/);
|
|
80
|
+
if (match && match[1]) {
|
|
81
|
+
return match[1];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validates a FHIR Reference element against allowed target resource types.
|
|
88
|
+
*/
|
|
89
|
+
export function validateReference(reference, path, allowedTypes) {
|
|
90
|
+
const issues = [];
|
|
91
|
+
if (!reference || typeof reference !== "object" || Array.isArray(reference)) {
|
|
92
|
+
issues.push({
|
|
93
|
+
severity: "error",
|
|
94
|
+
path,
|
|
95
|
+
code: "invalid-structure",
|
|
96
|
+
message: `Expected '${path}' to be a Reference object, got ${Array.isArray(reference) ? "array" : typeof reference}.`,
|
|
97
|
+
});
|
|
98
|
+
return issues;
|
|
99
|
+
}
|
|
100
|
+
const ref = reference;
|
|
101
|
+
if (ref.reference !== undefined && typeof ref.reference !== "string") {
|
|
102
|
+
issues.push({
|
|
103
|
+
severity: "error",
|
|
104
|
+
path: `${path}.reference`,
|
|
105
|
+
code: "invalid-type",
|
|
106
|
+
message: `Expected '${path}.reference' to be a string, got ${typeof ref.reference}.`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (ref.type !== undefined && typeof ref.type !== "string") {
|
|
110
|
+
issues.push({
|
|
111
|
+
severity: "error",
|
|
112
|
+
path: `${path}.type`,
|
|
113
|
+
code: "invalid-type",
|
|
114
|
+
message: `Expected '${path}.type' to be a string URI/type, got ${typeof ref.type}.`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (ref.display !== undefined && typeof ref.display !== "string") {
|
|
118
|
+
issues.push({
|
|
119
|
+
severity: "error",
|
|
120
|
+
path: `${path}.display`,
|
|
121
|
+
code: "invalid-type",
|
|
122
|
+
message: `Expected '${path}.display' to be a string, got ${typeof ref.display}.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Target type checking
|
|
126
|
+
const targets = allowedTypes ?? ALLOWED_REFERENCE_TARGETS[path];
|
|
127
|
+
if (targets && targets.length > 0 && !targets.includes("Resource")) {
|
|
128
|
+
const detectedType = extractReferenceType(ref);
|
|
129
|
+
if (detectedType && !targets.includes(detectedType)) {
|
|
130
|
+
issues.push({
|
|
131
|
+
severity: "error",
|
|
132
|
+
path,
|
|
133
|
+
code: "invalid-reference-type",
|
|
134
|
+
message: `Reference at '${path}' targets disallowed resource type '${detectedType}'. Allowed types: ${targets.join(", ")}.`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return issues;
|
|
139
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core validation types and interfaces for fhir-validator.
|
|
3
|
+
* Mirrors FHIR R4 OperationOutcome.issue structure.
|
|
4
|
+
*/
|
|
5
|
+
export type ValidationSeverity = "error" | "warning" | "information";
|
|
6
|
+
export interface ValidationIssue {
|
|
7
|
+
/** Severity level: 'error' (fails validation), 'warning', or 'information' */
|
|
8
|
+
severity: ValidationSeverity;
|
|
9
|
+
/** Dot-separated or JSON pointer-like field path (e.g. 'Patient.gender', 'Observation.subject') */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Machine-readable error code */
|
|
12
|
+
code: string;
|
|
13
|
+
/** Human-readable explanation of the issue */
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
/** True if no issues with severity 'error' exist */
|
|
18
|
+
valid: boolean;
|
|
19
|
+
/** List of all validation issues encountered */
|
|
20
|
+
issues: ValidationIssue[];
|
|
21
|
+
}
|
|
22
|
+
export type BindingStrength = "required" | "extensible" | "preferred" | "example";
|
|
23
|
+
export interface ProfileConstraints {
|
|
24
|
+
/** Identifier/name of the profile */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Target FHIR resource type this profile applies to */
|
|
27
|
+
resourceType: string;
|
|
28
|
+
/** Canonical URL or identifier of the profile */
|
|
29
|
+
url?: string;
|
|
30
|
+
/** List of element paths that MUST be present on the resource */
|
|
31
|
+
requiredElements?: string[];
|
|
32
|
+
/** Cardinality overrides per field path (e.g. { "name": { min: 1 }, "identifier": { min: 1 } }) */
|
|
33
|
+
cardinalityOverrides?: Record<string, {
|
|
34
|
+
min?: number;
|
|
35
|
+
max?: number;
|
|
36
|
+
}>;
|
|
37
|
+
/** Fixed or pattern values required at specific paths */
|
|
38
|
+
fixedValues?: Record<string, any>;
|
|
39
|
+
/** Binding strength overrides for coded elements */
|
|
40
|
+
bindingStrength?: Record<string, BindingStrength>;
|
|
41
|
+
}
|
|
42
|
+
export interface ValidateOptions {
|
|
43
|
+
/** Optional profile constraints to evaluate on top of base structural rules */
|
|
44
|
+
profile?: ProfileConstraints;
|
|
45
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProfileConstraints, ValidateOptions, ValidationIssue, ValidationResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detects whether an object contains a circular reference.
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasCircularReference(obj: unknown, seen?: WeakSet<object>): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a property path (e.g. "name", "category[0].coding[0].system") on an object.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
10
|
+
/**
|
|
11
|
+
* Validates profile constraints against a resource.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateProfileConstraints(resource: Record<string, unknown>, profile: ProfileConstraints, rootPath: string): ValidationIssue[];
|
|
14
|
+
/**
|
|
15
|
+
* Validates a FHIR R4 resource against base structural rules and optional profile constraints.
|
|
16
|
+
*
|
|
17
|
+
* @param resource The FHIR JSON object to validate.
|
|
18
|
+
* @param options Optional validation parameters (e.g. custom or illustrative profile).
|
|
19
|
+
* @returns ValidationResult containing `valid` boolean and list of `issues`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validate(resource: unknown, options?: ValidateOptions): ValidationResult;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { validatePatient } from "./base-definitions/patient.js";
|
|
2
|
+
import { validateObservation } from "./base-definitions/observation.js";
|
|
3
|
+
import { validateEncounter } from "./base-definitions/encounter.js";
|
|
4
|
+
import { validateCondition } from "./base-definitions/condition.js";
|
|
5
|
+
import { validateCoverage } from "./base-definitions/coverage.js";
|
|
6
|
+
import { validateClaim } from "./base-definitions/claim.js";
|
|
7
|
+
import { validateClaimResponse } from "./base-definitions/claim-response.js";
|
|
8
|
+
const BASE_VALIDATORS = {
|
|
9
|
+
Patient: validatePatient,
|
|
10
|
+
Observation: validateObservation,
|
|
11
|
+
Encounter: validateEncounter,
|
|
12
|
+
Condition: validateCondition,
|
|
13
|
+
Coverage: validateCoverage,
|
|
14
|
+
Claim: validateClaim,
|
|
15
|
+
ClaimResponse: validateClaimResponse,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Detects whether an object contains a circular reference.
|
|
19
|
+
*/
|
|
20
|
+
export function hasCircularReference(obj, seen = new WeakSet()) {
|
|
21
|
+
if (!obj || typeof obj !== "object")
|
|
22
|
+
return false;
|
|
23
|
+
if (seen.has(obj))
|
|
24
|
+
return true;
|
|
25
|
+
seen.add(obj);
|
|
26
|
+
for (const key of Object.keys(obj)) {
|
|
27
|
+
const val = obj[key];
|
|
28
|
+
if (val && typeof val === "object") {
|
|
29
|
+
if (hasCircularReference(val, seen))
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
seen.delete(obj);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolves a property path (e.g. "name", "category[0].coding[0].system") on an object.
|
|
38
|
+
*/
|
|
39
|
+
export function getNestedValue(obj, path) {
|
|
40
|
+
if (!obj || typeof obj !== "object")
|
|
41
|
+
return undefined;
|
|
42
|
+
// Normalize array access like a[0].b -> a.0.b
|
|
43
|
+
const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
|
|
44
|
+
const parts = normalizedPath.split(".");
|
|
45
|
+
let current = obj;
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
if (current === undefined || current === null)
|
|
48
|
+
return undefined;
|
|
49
|
+
current = current[part];
|
|
50
|
+
}
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validates profile constraints against a resource.
|
|
55
|
+
*/
|
|
56
|
+
export function validateProfileConstraints(resource, profile, rootPath) {
|
|
57
|
+
const issues = [];
|
|
58
|
+
if (profile.resourceType && profile.resourceType !== resource.resourceType) {
|
|
59
|
+
issues.push({
|
|
60
|
+
severity: "error",
|
|
61
|
+
path: `${rootPath}.resourceType`,
|
|
62
|
+
code: "profile-mismatch",
|
|
63
|
+
message: `Profile '${profile.name}' applies to '${profile.resourceType}', but resource is '${String(resource.resourceType)}'.`,
|
|
64
|
+
});
|
|
65
|
+
return issues;
|
|
66
|
+
}
|
|
67
|
+
// Check required elements
|
|
68
|
+
if (profile.requiredElements) {
|
|
69
|
+
for (const elem of profile.requiredElements) {
|
|
70
|
+
const val = getNestedValue(resource, elem);
|
|
71
|
+
if (val === undefined || val === null || val === "" || (Array.isArray(val) && val.length === 0)) {
|
|
72
|
+
issues.push({
|
|
73
|
+
severity: "error",
|
|
74
|
+
path: `${rootPath}.${elem}`,
|
|
75
|
+
code: "profile-required-element",
|
|
76
|
+
message: `Profile '${profile.name}' requires element '${elem}' to be present.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Check cardinality overrides
|
|
82
|
+
if (profile.cardinalityOverrides) {
|
|
83
|
+
for (const [elem, card] of Object.entries(profile.cardinalityOverrides)) {
|
|
84
|
+
const val = getNestedValue(resource, elem);
|
|
85
|
+
if (card.min !== undefined && card.min > 0) {
|
|
86
|
+
if (val === undefined || val === null) {
|
|
87
|
+
issues.push({
|
|
88
|
+
severity: "error",
|
|
89
|
+
path: `${rootPath}.${elem}`,
|
|
90
|
+
code: "profile-cardinality",
|
|
91
|
+
message: `Profile '${profile.name}' requires at least ${card.min} item(s) for '${elem}', but none found.`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (Array.isArray(val) && val.length < card.min) {
|
|
95
|
+
issues.push({
|
|
96
|
+
severity: "error",
|
|
97
|
+
path: `${rootPath}.${elem}`,
|
|
98
|
+
code: "profile-cardinality",
|
|
99
|
+
message: `Profile '${profile.name}' requires at least ${card.min} item(s) for '${elem}', found ${val.length}.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (card.max !== undefined && Array.isArray(val) && val.length > card.max) {
|
|
104
|
+
issues.push({
|
|
105
|
+
severity: "error",
|
|
106
|
+
path: `${rootPath}.${elem}`,
|
|
107
|
+
code: "profile-cardinality",
|
|
108
|
+
message: `Profile '${profile.name}' allows at most ${card.max} item(s) for '${elem}', found ${val.length}.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Check fixed values
|
|
114
|
+
if (profile.fixedValues) {
|
|
115
|
+
for (const [fixedPath, expectedValue] of Object.entries(profile.fixedValues)) {
|
|
116
|
+
const actualValue = getNestedValue(resource, fixedPath);
|
|
117
|
+
if (actualValue !== expectedValue) {
|
|
118
|
+
issues.push({
|
|
119
|
+
severity: "error",
|
|
120
|
+
path: `${rootPath}.${fixedPath}`,
|
|
121
|
+
code: "profile-fixed-value",
|
|
122
|
+
message: `Profile '${profile.name}' requires '${fixedPath}' to equal '${String(expectedValue)}', found '${String(actualValue)}'.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return issues;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates a FHIR R4 resource against base structural rules and optional profile constraints.
|
|
131
|
+
*
|
|
132
|
+
* @param resource The FHIR JSON object to validate.
|
|
133
|
+
* @param options Optional validation parameters (e.g. custom or illustrative profile).
|
|
134
|
+
* @returns ValidationResult containing `valid` boolean and list of `issues`.
|
|
135
|
+
*/
|
|
136
|
+
export function validate(resource, options) {
|
|
137
|
+
const issues = [];
|
|
138
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
issues: [
|
|
142
|
+
{
|
|
143
|
+
severity: "error",
|
|
144
|
+
path: "resource",
|
|
145
|
+
code: "invalid-structure",
|
|
146
|
+
message: `Expected resource to be a JSON object, got ${Array.isArray(resource) ? "array" : typeof resource}.`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (hasCircularReference(resource)) {
|
|
152
|
+
return {
|
|
153
|
+
valid: false,
|
|
154
|
+
issues: [
|
|
155
|
+
{
|
|
156
|
+
severity: "error",
|
|
157
|
+
path: "resource",
|
|
158
|
+
code: "circular-reference",
|
|
159
|
+
message: "Resource contains an invalid circular reference.",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const res = resource;
|
|
165
|
+
if (typeof res.resourceType !== "string" || !res.resourceType.trim()) {
|
|
166
|
+
return {
|
|
167
|
+
valid: false,
|
|
168
|
+
issues: [
|
|
169
|
+
{
|
|
170
|
+
severity: "error",
|
|
171
|
+
path: "resource.resourceType",
|
|
172
|
+
code: "missing-resource-type",
|
|
173
|
+
message: "Missing or invalid required 'resourceType' string property.",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const resourceType = res.resourceType.trim();
|
|
179
|
+
const baseValidator = BASE_VALIDATORS[resourceType];
|
|
180
|
+
if (!baseValidator) {
|
|
181
|
+
issues.push({
|
|
182
|
+
severity: "error",
|
|
183
|
+
path: `${resourceType}.resourceType`,
|
|
184
|
+
code: "unsupported-resource-type",
|
|
185
|
+
message: `Resource type '${resourceType}' is not supported by fhir-validator v1. Supported types: ${Object.keys(BASE_VALIDATORS).join(", ")}.`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// 1. Run base structural validation
|
|
190
|
+
issues.push(...baseValidator(res, resourceType));
|
|
191
|
+
}
|
|
192
|
+
// 2. Run profile constraints if provided
|
|
193
|
+
if (options?.profile) {
|
|
194
|
+
issues.push(...validateProfileConstraints(res, options.profile, resourceType));
|
|
195
|
+
}
|
|
196
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
197
|
+
return {
|
|
198
|
+
valid: !hasErrors,
|
|
199
|
+
issues,
|
|
200
|
+
};
|
|
201
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peerbits/fhir-validator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Structural, cardinality, and reference validation for FHIR R4 resources — a fast base-spec validator with a pluggable illustrative-profile mechanism (not a full conformance engine)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"build": "tsc -p tsconfig.json",
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test": "vitest run"
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
27
28
|
},
|
|
28
29
|
"repository": {
|
|
29
30
|
"type": "git",
|