diff --git a/CHANGELOG.md b/CHANGELOG.md index c826c70..3bd9d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.1.0] - 2026-02-24 + +### Added +- **XML Parser**: New `parseXML(xmlContent, ID, [callback])` method on `hl7Parser` that parses HL7 v2 XML-encoded messages (namespace `urn:hl7-org:v2xml`) and returns a standard `Hl7Message` object, fully compatible with all existing methods (`get`, `getSegments`, `toMappedObject`, etc.). +- **FHIR Transformation**: New `toFHIR()` method on `Hl7Message` that converts a parsed HL7 v2 message into a FHIR R4 Bundle (JSON), mapping: + - MSH segment → `MessageHeader` resource (id, eventCoding, source, destination) + - PID segment → `Patient` resource (identifier, name, birthDate, gender, address, telecom) +- **33 new unit tests** covering XML parsing, FHIR output, error handling, callback API, and `toMappedObject` compatibility. +- `toFHIR()` works on both XML-parsed and ER7-parsed messages. + ## [2.0.0] - 2026-02-24 ### Changed diff --git a/README.md b/README.md index 4390ec8..6cb7642 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ NodeJS Library for parsing HL7 Messages **Homepage:** [https://loksly.github.io/nodehl7/](https://loksly.github.io/nodehl7/) -This library provides an easy way to parse HL7 Messages v.2.x, text-based, no XML format. +This library provides an easy way to parse HL7 Messages v.2.x, both text-based (ER7) and XML formats. Note there is another package named [node-hl7](https://github.com/ekryski/node-hl7) that provides a different API. ## Features @@ -19,7 +19,9 @@ Note there is another package named [node-hl7](https://github.com/ekryski/node-h - ✅ **Promise-based API**: Modern async/await support with backward-compatible callbacks - ✅ **Dual Module Format**: Both CommonJS and ES Modules supported - ✅ **Event Emitter**: Subscribe to parse events -- ✅ **HL7 v2.x**: Support for HL7 version 2.x text-based messages +- ✅ **HL7 v2.x ER7**: Support for HL7 version 2.x text-based (pipe-delimited) messages +- ✅ **HL7 v2.x XML**: Support for HL7 version 2.x XML-encoded messages (`parseXML`) +- ✅ **FHIR Transformation**: Convert parsed HL7 messages to FHIR R4 Bundle JSON (`toFHIR`) - ✅ **MLLP Network Transport**: Built-in MLLP server and client for real-time HL7 message exchange over TCP/IP - ✅ **TLS/SSL Support**: Optional encrypted connections for secure HL7 communication @@ -293,6 +295,39 @@ hl7parser.parse(messageContent, 'msg-001', (err, message) => { }); ``` +##### `parseXML(xmlContent, ID, callback?)` + +Parses an HL7 v2 XML-encoded message string (namespace `urn:hl7-org:v2xml`). + +**Returns:** `Promise` + +**Parameters:** +- `xmlContent` (string): The HL7 v2 XML message content +- `ID` (string): An identifier for the message +- `callback` (function, optional): Callback function `(err, Hl7Message) => void` + +The returned `Hl7Message` is fully compatible with all existing methods (`get`, `getSegments`, `toMappedObject`, `toFHIR`, etc.). + +```javascript +// From a file +const xml = require('fs').readFileSync('./message.xml', 'utf8'); +const message = await hl7parser.parseXML(xml, 'msg-001'); +console.log(message.get('MSH', 'Version ID')); // '2.5' + +// From a string +const xmlContent = ` + + + | + ^~\\& + SENDER + MSG001 + 2.5 + +`; +const message = await hl7parser.parseXML(xmlContent, 'msg-001'); +``` + ##### `parseFile(filepath, callback?)` Parses an HL7 message from a file. @@ -323,6 +358,7 @@ hl7parser.parseFile('./file.hl7', (err, message) => { - **`get(segmentname, fieldname, joinChar)`**: Returns the field value. If it's an array, joins it using `joinChar` as separator. - **`getSegmentAt(index)`**: Returns the segment at the specified position. - **`getSegments(segmentname)`**: Returns all segments with the specified name. +- **`toFHIR()`**: Converts the message to a FHIR R4 Bundle (JSON object). Maps MSH → `MessageHeader` and PID → `Patient` resources. ```javascript const message = await hl7parser.parseFile('./file.hl7'); @@ -341,8 +377,32 @@ const firstSegment = message.getSegmentAt(0); // Get number of segments const segmentCount = message.size(); + +// Convert to FHIR Bundle +const fhirBundle = message.toFHIR(); +console.log(fhirBundle.resourceType); // 'Bundle' +console.log(fhirBundle.entry[0].resource.resourceType); // 'MessageHeader' +console.log(fhirBundle.entry[1].resource.resourceType); // 'Patient' ``` +#### FHIR Field Mapping + +`toFHIR()` produces a FHIR R4 Bundle of type `message` with the following mappings: + +| HL7 v2 Field | FHIR Field | +|---|---| +| MSH-10 Message Control ID | `MessageHeader.id` | +| MSH-3 Sending Application | `MessageHeader.source.name` | +| MSH-4 Sending Facility | `MessageHeader.source.software` | +| MSH-5 Receiving Application | `MessageHeader.destination[0].name` | +| MSH-9 Message Type | `MessageHeader.eventCoding.code` | +| PID-3 Patient Identifier List | `Patient.identifier[0].value` | +| PID-5 Patient Name | `Patient.name[0].family` / `.given` | +| PID-7 Date of Birth | `Patient.birthDate` (YYYY-MM-DD) | +| PID-8 Gender (M/F/O/U) | `Patient.gender` (male/female/other/unknown) | +| PID-11 Patient Address | `Patient.address[0]` | +| PID-13 Phone Number (Home) | `Patient.telecom[0]` | + ### Hl7Segment #### Methods diff --git a/dist/esm/hl7.d.ts b/dist/esm/hl7.d.ts index 099c12b..3def7ad 100644 --- a/dist/esm/hl7.d.ts +++ b/dist/esm/hl7.d.ts @@ -33,6 +33,7 @@ declare class Hl7Message { getSegments(segmentName: string): HL7Segment[]; getSegments(segmentName: string, nmbr: number): HL7Segment | null; getSegments(segmentName: string, nmbr: number, fieldName: string, joinChar?: string): string | string[] | null; + toFHIR(): Record; } declare class HL7Segment { typeofSegment: string; @@ -54,6 +55,7 @@ declare class hl7Parser extends EventEmitter { HL7Segment: typeof HL7Segment; constructor(options?: HL7ParserOptions); parse(messageContent: string, ID: string, wrappedDone?: (err: unknown, hl7msg?: Hl7Message) => void): Promise; + parseXML(xmlContent: string, ID: string, wrappedDone?: (err: unknown, hl7msg?: Hl7Message) => void): Promise; parseFile(filepath: string, wrappedDone?: (err: unknown, message?: Hl7Message) => void): Promise; } export { MLLPServer, MLLPClient, wrap as mllpWrap, unwrap as mllpUnwrap, VT, FS_CR } from './mllp'; diff --git a/dist/esm/hl7.d.ts.map b/dist/esm/hl7.d.ts.map index a4b7d1e..d66d83b 100644 --- a/dist/esm/hl7.d.ts.map +++ b/dist/esm/hl7.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"hl7.d.ts","sourceRoot":"","sources":["../../src/hl7.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"hl7.d.ts","sourceRoot":"","sources":["../../src/hl7.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/esm/hl7.js b/dist/esm/hl7.js index e12c9f5..c03646f 100644 --- a/dist/esm/hl7.js +++ b/dist/esm/hl7.js @@ -4,6 +4,125 @@ import * as encoding from 'encoding'; import * as fs from 'fs'; import { allSegmentDefs } from './segments'; let validSegmentsName = []; +function decodeXMLEntities(text) { + return text + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&'); +} +function parseXMLDocument(xmlStr) { + xmlStr = xmlStr.replace(/<\?[\s\S]*?\?>/g, ''); + let commentStart = xmlStr.indexOf('', commentStart); + if (commentEnd < 0) { + xmlStr = xmlStr.slice(0, commentStart); + break; + } + xmlStr = xmlStr.slice(0, commentStart) + xmlStr.slice(commentEnd + 3); + commentStart = xmlStr.indexOf('', commentStart); + if (commentEnd < 0) { + xmlStr = xmlStr.slice(0, commentStart); + break; + } + xmlStr = xmlStr.slice(0, commentStart) + xmlStr.slice(commentEnd + 3); + commentStart = xmlStr.indexOf('', commentStart); + if (commentEnd < 0) { + xmlStr = xmlStr.slice(0, commentStart); + break; + } + xmlStr = xmlStr.slice(0, commentStart) + xmlStr.slice(commentEnd + 3); + commentStart = xmlStr.indexOf('