@peerbits/fhir-validator 1.0.1 → 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 +2 -0
- package/dist/base-definitions/bundle.d.ts +2 -0
- package/dist/base-definitions/bundle.js +41 -0
- package/dist/base-definitions/observation.js +29 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/validate.js +13 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @peerbits/fhir-validator
|
|
2
2
|
|
|
3
|
+
Version 1.1 validates Bundle structure and recursively validates bundled resources, including transaction/batch request details. Observation checks now cover periods, components, finite quantities, UCUM codes, and FHIR choice-field cardinality.
|
|
4
|
+
|
|
3
5
|
> Fast, lightweight structural, cardinality, and reference validation for FHIR R4 resources.
|
|
4
6
|
|
|
5
7
|
[](https://github.com/PeerbitsSolution/fhir-validator/actions)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const BUNDLE_TYPES = ['document', 'message', 'transaction', 'transaction-response', 'batch', 'batch-response', 'history', 'searchset', 'collection'];
|
|
2
|
+
export function validateBundle(resource, path = 'Bundle') {
|
|
3
|
+
const issues = [];
|
|
4
|
+
if (resource.resourceType !== 'Bundle')
|
|
5
|
+
issues.push({ severity: 'error', path: `${path}.resourceType`, code: 'invalid-resource-type', message: "Expected resourceType 'Bundle'." });
|
|
6
|
+
if (typeof resource.type !== 'string' || !BUNDLE_TYPES.includes(resource.type)) {
|
|
7
|
+
issues.push({ severity: 'error', path: `${path}.type`, code: 'invalid-value', message: `Bundle.type must be one of: ${BUNDLE_TYPES.join(', ')}.` });
|
|
8
|
+
}
|
|
9
|
+
if (resource.entry !== undefined && !Array.isArray(resource.entry)) {
|
|
10
|
+
issues.push({ severity: 'error', path: `${path}.entry`, code: 'invalid-type', message: 'Bundle.entry must be an array.' });
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(resource.entry)) {
|
|
13
|
+
resource.entry.forEach((entry, index) => {
|
|
14
|
+
const entryPath = `${path}.entry[${index}]`;
|
|
15
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
16
|
+
issues.push({ severity: 'error', path: entryPath, code: 'invalid-structure', message: 'Bundle entry must be an object.' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const value = entry;
|
|
20
|
+
if (!value.resource || typeof value.resource !== 'object' || Array.isArray(value.resource)) {
|
|
21
|
+
issues.push({ severity: 'error', path: `${entryPath}.resource`, code: 'required', message: 'Bundle entry requires a resource object.' });
|
|
22
|
+
}
|
|
23
|
+
if (resource.type === 'transaction' || resource.type === 'batch') {
|
|
24
|
+
if (!value.request || typeof value.request !== 'object' || Array.isArray(value.request)) {
|
|
25
|
+
issues.push({ severity: 'error', path: `${entryPath}.request`, code: 'required', message: `${resource.type} Bundle entry requires request.` });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const request = value.request;
|
|
29
|
+
const methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
30
|
+
if (typeof request.method !== 'string' || !methods.includes(request.method)) {
|
|
31
|
+
issues.push({ severity: 'error', path: `${entryPath}.request.method`, code: 'invalid-value', message: `Bundle request.method must be one of: ${methods.join(', ')}.` });
|
|
32
|
+
}
|
|
33
|
+
if (typeof request.url !== 'string' || !request.url.trim()) {
|
|
34
|
+
issues.push({ severity: 'error', path: `${entryPath}.request.url`, code: 'required', message: 'Bundle request.url must be a non-empty string.' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return issues;
|
|
41
|
+
}
|
|
@@ -15,6 +15,12 @@ const VALID_OBSERVATION_STATUSES = [
|
|
|
15
15
|
*/
|
|
16
16
|
export function validateObservation(resource, path = "Observation") {
|
|
17
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." });
|
|
18
24
|
if (resource.resourceType !== "Observation") {
|
|
19
25
|
issues.push({
|
|
20
26
|
severity: "error",
|
|
@@ -104,7 +110,7 @@ export function validateObservation(resource, path = "Observation") {
|
|
|
104
110
|
}
|
|
105
111
|
else {
|
|
106
112
|
const q = resource.valueQuantity;
|
|
107
|
-
if (q.value !== undefined && typeof q.value !== "number") {
|
|
113
|
+
if (q.value !== undefined && (typeof q.value !== "number" || !Number.isFinite(q.value))) {
|
|
108
114
|
issues.push({
|
|
109
115
|
severity: "error",
|
|
110
116
|
path: `${path}.valueQuantity.value`,
|
|
@@ -136,6 +142,24 @@ export function validateObservation(resource, path = "Observation") {
|
|
|
136
142
|
message: `Expected '${path}.valueQuantity.code' to be a string.`,
|
|
137
143
|
});
|
|
138
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." });
|
|
139
163
|
}
|
|
140
164
|
}
|
|
141
165
|
// valueCodeableConcept
|
|
@@ -178,7 +202,10 @@ export function validateObservation(resource, path = "Observation") {
|
|
|
178
202
|
}
|
|
179
203
|
if (c.valueQuantity !== undefined) {
|
|
180
204
|
const q = c.valueQuantity;
|
|
181
|
-
if (q
|
|
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)) {
|
|
182
209
|
issues.push({
|
|
183
210
|
severity: "error",
|
|
184
211
|
path: `${compPath}.valueQuantity.value`,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* FHIR R4 resources. Explicitly NOT a full conformance engine — see
|
|
4
4
|
* docs/KNOWN_LIMITATIONS.md and developer handover spec.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "1.0
|
|
6
|
+
export declare const VERSION = "1.1.0";
|
|
7
7
|
export { validate, getNestedValue, validateProfileConstraints, hasCircularReference } from "./validate.js";
|
|
8
8
|
export type { ValidationIssue, ValidationResult, ValidationSeverity, ProfileConstraints, BindingStrength, ValidateOptions, } from "./types.js";
|
|
9
9
|
export { validateCoding, validateCodeableConcept } from "./coding.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* FHIR R4 resources. Explicitly NOT a full conformance engine — see
|
|
4
4
|
* docs/KNOWN_LIMITATIONS.md and developer handover spec.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "1.0
|
|
6
|
+
export const VERSION = "1.1.0";
|
|
7
7
|
// Main API
|
|
8
8
|
export { validate, getNestedValue, validateProfileConstraints, hasCircularReference } from "./validate.js";
|
|
9
9
|
// Coding & Reference Helpers
|
package/dist/validate.js
CHANGED
|
@@ -5,6 +5,7 @@ import { validateCondition } from "./base-definitions/condition.js";
|
|
|
5
5
|
import { validateCoverage } from "./base-definitions/coverage.js";
|
|
6
6
|
import { validateClaim } from "./base-definitions/claim.js";
|
|
7
7
|
import { validateClaimResponse } from "./base-definitions/claim-response.js";
|
|
8
|
+
import { validateBundle } from "./base-definitions/bundle.js";
|
|
8
9
|
const BASE_VALIDATORS = {
|
|
9
10
|
Patient: validatePatient,
|
|
10
11
|
Observation: validateObservation,
|
|
@@ -13,6 +14,7 @@ const BASE_VALIDATORS = {
|
|
|
13
14
|
Coverage: validateCoverage,
|
|
14
15
|
Claim: validateClaim,
|
|
15
16
|
ClaimResponse: validateClaimResponse,
|
|
17
|
+
Bundle: validateBundle,
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* Detects whether an object contains a circular reference.
|
|
@@ -188,6 +190,17 @@ export function validate(resource, options) {
|
|
|
188
190
|
else {
|
|
189
191
|
// 1. Run base structural validation
|
|
190
192
|
issues.push(...baseValidator(res, resourceType));
|
|
193
|
+
if (resourceType === "Bundle" && Array.isArray(res.entry)) {
|
|
194
|
+
res.entry.forEach((entry, index) => {
|
|
195
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
196
|
+
return;
|
|
197
|
+
const nested = entry.resource;
|
|
198
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested))
|
|
199
|
+
return;
|
|
200
|
+
const nestedResult = validate(nested);
|
|
201
|
+
nestedResult.issues.forEach((issue) => issues.push({ ...issue, path: `Bundle.entry[${index}].resource.${issue.path}` }));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
191
204
|
}
|
|
192
205
|
// 2. Run profile constraints if provided
|
|
193
206
|
if (options?.profile) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peerbits/fhir-validator",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"test": "vitest run",
|
|
27
|
-
"
|
|
27
|
+
"prepack": "npm run build && npm run typecheck && npm test",
|
|
28
|
+
"prepublishOnly": "npm run lint && npm run prepack"
|
|
28
29
|
},
|
|
29
30
|
"repository": {
|
|
30
31
|
"type": "git",
|
|
@@ -42,9 +43,9 @@
|
|
|
42
43
|
"@typescript-eslint/parser": "^8.0.0",
|
|
43
44
|
"eslint": "^9.0.0",
|
|
44
45
|
"typescript": "^5.5.0",
|
|
45
|
-
"vitest": "^
|
|
46
|
+
"vitest": "^4.0.0"
|
|
46
47
|
},
|
|
47
48
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
49
|
+
"node": ">=20"
|
|
49
50
|
}
|
|
50
51
|
}
|