@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 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
  [![CI](https://github.com/PeerbitsSolution/fhir-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/PeerbitsSolution/fhir-validator/actions)
@@ -0,0 +1,2 @@
1
+ import type { ValidationIssue } from '../types.js';
2
+ export declare function validateBundle(resource: Record<string, unknown>, path?: string): ValidationIssue[];
@@ -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 && typeof q.value !== "number" && q.value !== undefined) {
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.1";
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.1";
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.1",
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
- "prepublishOnly": "npm run build"
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": "^2.0.0"
46
+ "vitest": "^4.0.0"
46
47
  },
47
48
  "engines": {
48
- "node": ">=18"
49
+ "node": ">=20"
49
50
  }
50
51
  }