HL7 v2
The @gofer-engine/hl7
is a strongly typed Msg
TypeScript class to encode,
decode, extrapolate, transform, and map over HL7 message data.
This library package was developed as an independent library within
@gofer-engine
. This allows developers to use this package without also needing
@gofer-engine/engine
.
Formerly released as ts-hl7
, this package was the initial project which spurred
the creation of Gofer Engine to bring the simplicity of ts-hl7
into a complete
working Engine and not just a message encapsulation class.
Purpose
HL7 v2.x has a unique syntax and message structure. Usually, HL7 messages are encoded and decoded into and out of XML structures. While XML structures are flexible enough to handle the unique message structure. They are harder to work with for extrapolating and transforming over the more commonly used JSON data structure. We knew we wanted to store the message data in a format that was compact but could also be easily queried directly and deeply within modern graph and document databases such as MongoDB, SurrealDB, and Dgraph which also work directly with JSON data structures. So for these reasons, we developed this package.
If you are unfamiliar with HL7, the founder of Gofer Engine, Anthony Master, has written an article providing a introduction for beginners into the world of HL7: What is HL7
Installation
To use with Node projects, open a terminal/command prompt inside of your node project directory and run
npm install @gofer-engine/hl7
If you want to use this library in a TS/JS project, but outside of a node project, please contact me and we will work out the steps to follow. I have not had a need for this yet.
Usage
import Msg from '@gofer-engine/hl7'
// using fs to read sample.hl7 file
import fs from 'fs'
const HL7_String = fs.readFileSync('./sample.hl7', 'utf8')
const msg = new Msg(HL7_String)
Documentation
Decoding
HL7 messages can be decoded by simply passing the raw string message to the Msg constructor:
const msg = new Msg(HL7_string)
To see the JSON decoded message, you can use the json() method:
const json = new Msg(HL7_string).json()
console.log(JSON.stringify(json, undefined, 2))
You can pass an argument to the json method to normalize the JSON format
const normalized = new Msg(HL7_string).json(true)
console.log(JSON.stringify(normalized, undefined, 2))
If you have a JSON object that you want to replace the decoded JSON object, you can use the setMsg method:
const foo = new Msg(HL7_string)
const bar = new Msg('MSH|^~\\&|...')
bar.setMsg(foo.json())
console.log(bar.json())
Encoding
To encode a message back to an HL7 string, you can use the ‘toString’ method on the Msg class:
const msg = new Msg(HL7_string)
console.log(msg.toString())
Extrapolating
HL7 uses paths to reference data inside of the HL7 message structure. The path
is a string that is a combination of the segment name, segment iteration index,
field index, field iteration index, component index, and subcomponent index.
Iteration indexes are surrounded by brackets ([...]
). The other path parts are
separated by either a period (.
) or a dash (-
). The path is 1-indexed, meaning
the first segment iteration is 1
, the first field is 1
, the first field
iteration is 1
, the first component is 1
, and the first subcomponent is 1
. The
segment name is 3
upper case characters. A path can be specific down to the
subcomponent level with optional iteration indexes or can be as general as just
the segment name. The following are all valid paths (may not be valid HL7
schemed messages):
MSH
, MSH-3
, MSH.7
, MSH.9.1
, MSH.9-2
, MSH.10
, STF-2.1
, STF-2[2].1
, STF-3.1
, STF-11[2]
, LAN[1]
, LAN[2].3
, LAN[3].6[1].1
The Msg
class provides methods to structure and destructure paths.
type Paths = {
segmentName?: string
segmentIteration?: number
fieldPosition?: number
fieldIteration?: number
componentPosition?: number
subComponentPosition?: number
}
Msg.paths = (path?: string) => Paths
Msg.toPath = (path?: Paths) => string
Iteration indexes can be defined as [1]
even for non-iterative paths. Simplified
1
based paths are also supported even if the value is not deeply nested. For
example ZZZ[1]-1[1].1
might reference the same value (Source) as the following:
ZZZ[1]-1[1]
, ZZZ[1]-1
, ZZZ-1
, ZZZ-1[1]
, and ZZZ-1.1
in the the following HL7
messages:
MSH|^~\&|HL7REG|UH|HL7LAB|CH|200702280700||PMU^B01^PMU_B01|MSGID002|P|2.5.1|
EVN|B01|200702280700|
STF||U2246^^^PLW~111223333^^^USSSA^SS|HIPPOCRATES^HAROLD^H^JR^DR^M.D.|P|M|19511004|A|^ICU|^MED|(555)555-1003X345^C^O~(555)555-3334^C^H~(555)555-1345X789^C^B|1003 HEALTHCARE DRIVE^SUITE 200^ANNARBOR^MI^98199^H~3029 HEALTHCARE DRIVE^^ANNARBOR^MI^98198^O|19890125^DOCTORSAREUS MEDICAL SCHOOL&L01||PMF88123453334|74160.2326@COMPUSERV.COM|B
PRA||^HIPPOCRATES FAMILY PRACTICE|ST|I|OB/GYN^STATE BOARD OF OBSTETRICS AND GYNECOLOGY^C^19790123|1234887609^UPIN~1234987^CTY^MECOSTA~223987654^TAX~1234987757^DEA~12394433879^MDD^CA|ADMIT&T&ADT^MED&&L2^19941231~DISCH&&ADT^MED&&L2^19941231|
AFF|1|AMERICAN MEDICAL ASSOCIATION|123 MAIN STREET^^OUR TOWN^CA^98765^U.S.A.^M |19900101|
LAN|1|ESL^SPANISH^ISO639|1^READ^HL70403|1^EXCELLENT^HL70404|
LAN|2|ESL^SPANISH^ISO639|2^WRITE^HL70403|2^GOOD^HL70404|
LAN|3|FRE^FRENCH^ISO639|3^SPEAK^HL70403|3^FAIR^HL70404|
EDU|1|BA^BACHELOR OF ARTS^HL70360|19810901^19850601|YALE UNIVERSITY^L|U^HL70402|456 CONNECTICUT AVENUE^^NEW HAVEN^CO^87654^U.S.A.^M|
EDU|2|MD^DOCTOR OF MEDICINE^HL70360|19850901^19890601|HARVARD MEDICAL SCHOOL^L |M^HL70402|123 MASSACHUSETTS AVENUE^CAMBRIDGE^MA^76543^U.S.A.^M|
ZZZ|Source|HL7 Version 2.5.1 Standard^Chapter&15&Personnel Management^Section&5&Example Transactions^Page&15-40^Date&200704
The Msg
class exposes the get
method that accepts a string which can be comprised of the following:
- Segment Name
- Segment Repition Index
- Field Index
- Field Repition Index
- Component Index
- Sub-Component Index
Repetition Indexes are optional and can be omitted. If omitted on a repeating segment/field, then an array of values will be returned.
For example to get all LAN
segments you can use the get path LAN
.
If you wanted just one of the segments, you would add in the repetition index,
for example, LAN[2]
would return the second LAN
segment.
Repetition Indexes are always syntactically wrapped in square brackets. e.g. [2]
Field, Component, and Sub-Component are optional and can be omitted. If omitted and the field/component is made up of smaller units, then an array of values will be returned.
For example, to get all the components of the language codes in the 1st LAN
segment you can use the get path LAN[1]-2
Field, Component, and Sub-Component are always prefixed with either a dot or a
hyphen. e.g. .2
or -2
If there is only a single larger component, then the smaller divisions can be omitted.
For example, EVN-1.1.1
will return the same as EVN-1.1
or EVN-1
Fields can also be repetitive and can be accessed by using the field repetition
index. For example, STF-10[1]
will return the 1st repetition of the 10th field
in the SFT
segment.
Compare how these different extracted paths compare:
import Msg from './src/'
import fs from 'fs'
const HL7 = fs.readFileSync('./sample.hl7', 'utf8')
const msg = new Msg(HL7)
console.log('STF-10[1].1:', msg.get('STF-10[1].1')) // (555)555-1003X345
console.log('STF[1]-10[1]:', msg.get('STF[1]-10[1]')) // ['(555)555-1003X345','C','O']
console.log('STF-10[1]:', msg.get('STF-10[1]')) // ['(555)555-1003X345','C','O']
console.log('STF-10.1:', msg.get('STF-10.1')) // ['(555)555-1003X345','(555)555-3334','(555)555-1345X789']
console.log('STF.10.1:', msg.get('STF.10.1')) // ['(555)555-1003X345','(555)555-3334','(555)555-1345X789']
console.log('LAN:', msg.get('LAN')) // [Seg{ ... },Seg{ ... },Seg{ ... }]
console.log('LAN[2]:', msg.get('LAN[2]')) // Seg{ ... }
console.log('LAN-2.1:', msg.get('LAN-2.1')) // ['ESL','ESL','FRE']
console.log('LAN[1]-2.1:', msg.get('LAN[1]-2.1')) // 'ESL'
console.log('LAN-2:', msg.get('LAN-2')) // [['ESL','SPANISH','ISO639'],['ESL','SPANISH','ISO639'],['FRE','FRENCH','ISO639']]
console.log('ZZZ-2.2', msg.get('ZZZ-2.2')) // ['Chapter','15','Personnel Management' ]
console.log('ZZZ-2.2.1', msg.get('ZZZ-2.2.1')) // 'Chapter'
Unless a path is fully defined including all repetition indexes, then the type returned could be an array or a string.
If the path is only a segment name, then either a single Segment (Seg
) class or
an array of Segment (Seg
) classes will be returned. See Segment Class. If you
want to be sure to get back a singular string value, then you should use the
most specific path possible. For example, if you want to get the first component
of the first field of the first segment iteration of the MSH
segment, then you
should use the path MSH[1].1.1
. If you use the path MSH.1.1
, and there are
multiple MSH
segments, then you will get back an array of strings, one for each
segment iteration.
You can use wrap the get
method around the toPath
method, to specify path
components individually instead of a concatenated string.
const event = msg.get(msg.toPath({
segmentName: 'MSH',
segmentIteration: 1, // defaults to 1
fieldPosition: 9,
fieldIteration: 1, // defaults to 1
componentPosition: 2,
subComponentPosition: 1, // defaults to 1
}))
Transforming
You can use the following methods to transform the message. Each method returns the self-class instance, so you can chain the methods together as needed.
addSegment
addSegment(segment: string | Segment | Seg | Segs, after?: number | string)
—
Adds a HL7 encoded segment string or Segment json or Seg(s) class(es) after the
indicated position in the message.
// parses the HL7 segment(s) and adds the segment(s) at the end of the message
msg.addSegment('ZZZ|Test')
// add the simplified json segment to the beginning of the message
const segment = ['MSH', '|', '^~\\&',,,,,,,['PMU','B01','PMU_B01'],,,'2.5.1',]
msg.addSegment(segment, 0)
// add the segment class after the first OBX segment
const seg = new Seg(['NTE', 1, null, 'Some Note'])
msg.addSegment(seg, 'OBX')
// parse and adds multiple segments after the second OBX segment
const notes = `NTE|1||Multi-line
NTE|1||Note`
msg.addSegment(notes, 'OBX[2]')
// add multiple simplified json segments after the SPM, OBR, OBX[2], TCD segment sequence
const noteArray = [['NTE', 1, , 'Foo'], ['NTE', 1, , 'Bar']]
msg.addSegment(noteArray, 'SPM:OBR:OBX[2]:TCD')
If the path sequence is not found then an error will be returned.
transform
transform(limit: { restrict: IMsgFieldList, remove: IMsgFieldList })
—
Transforms the message by restricting to only certain elements and/or removing
certain elements. See the comments in the example below.
msg.transform({
restrict: {
MSH: () => true, // if function equates to true, keep whole segment
LAN: 3, // an integer keeps only the nth iteration of the segment (LAN[3])
ZZZ: { // an object keeps only certain fields of the segment
1: true, // true keeps the entire field as is (ZZZ-1)
2: [1, 5] // an array keeps only certain components of the field (ZZZ-2.1, ZZZ-2.5)
},
STF: {
2: [1, 4],
3: [], // an empty array keeps no components, same as if key was undefined
4: [2],
5: true,
10: 1, // an integer keeps only the nth repeition of the field
11: (f) => f?.[5] === 'O' // NOICE! `f` here is 0-indexed. This is keeping only the repetition that has STF-11.6 === 'O'
},
EDU: true, // if true, keep whole segment as is
// Any segments not specifically listed in a `restrict` object are removed.
},
remove: {
LAN: 2,
EDU: {
1: true, // deletes EDU-1
2: [3], // deletes EDU-2.3
3: (f) => {
if (Array.isArray(f) && typeof f[0] === 'string')
return f[0] > '19820000' // NOTICE! `f` here is 0-indexed. This looks at EDU-3.1
return false
}, // A function deletes when returns true. This is deleting EDU-3 when EDU-3.1 is greater than the year 1982
4: 2 // deletes EDU-4[2]
}
}
})
copy
copy(fromPath: string, toPath: string)
— Copies the value from one path to
another. Copy allows for deep copy copying repeating fields, components, and
sub-components that may exist.
msg.copy('ZZZ-2', 'MSH-3')
move
move(fromPath: string, toPath: string)
— Moves the value from one path to
another. Move is the same as copying the value to a temporary variable, deleting
the value, writing the variable to the new path, and then clearing the temporary
variable.
msg.move('LAN[1]-3[2]', 'LAN[1]-4')
delete
delete(path: string)
— Removes the value at the given path
msg.delete('EDU-6.6').delete('ZZZ')
set
set(path: string, value: string)
— Sets the value at the given path
msg.set('LAN[3]-4.1', '2')
msg.set('LAN[3]-4.2', 'GOOD')
setJSON
setJSON(path: string, value: MsgValue)
— Sets the value at the given path with
a JSON object in case of sub-items.
msg.setJSON('LAN-4', ['3', 'FAIR', 'HL70404'])
map
map(path: string, dictionary: string | Record<string, string> | string[] | <T>(val: T, index: number) => T, { iteration: boolean })
— Sets the value at
the given path with a mapped value. If the map is a string, then the value will
be replaced with the map. If the map is a key-value object, then the value will
be replaced with the value of the key-value object where the existing value
matches the key. If the map is an array, then the value will be replaced with
the value of the array at the index of the value converted to an integer
(1-based indexing). If the map is a function, then the value will be replaced
with the return value of the function. The function will be passed the value and
the index of the value (1-based indexing). If the iteration option is set to
true, then the function will be called for each iteration of the path. If the
iteration option is set to false, then the function will only be called once for
the path. Defaults to false.
// Dictionary Map an HL7 path using a key-value object.
msg.map('LAN-2.2', { ENGLISH: 'English' })
// Integer Map an HL7 path using an array. Array is treated as 1-based indexed.
msg.map('LAN-4.1', ['Excellent', 'Fair', 'Poor'])
// Function Map an HL7 path using a custom defined function that receives the original value and returns the new value.
msg.map('ZZZ-1', (orig) => orig.toUpperCase())
msg.map('LAN-4.2', {
EXCELLENT: 'BEST',
GOOD: 'BETTER',
FAIR: 'GOOD',
})
setIteration
setIteration<Y>(path: string, map: Y[] | ((val: Y, i: number) => Y), { allowLoop: boolean })
— Sets the iteration of the path to the given map. If the
map is an array, then the iteration will be set to the iterated index of the
array (1-based). If the map is a function, then the iteration will be set to the
return value of the function. The function will be passed the value and the
index of the iteration (1-based indexing). If the allowLoop option is set to
true and the array length is less than the iterations of the path, then the
array will be looped over to fill the iterations. If the allowLoop option is set
to false and the array length is less than the iterations of the path, then the
remaining iterations will be set to empty. Defaults to false.
// Function Map an HL7 path using a custom defined function that receives the original value and returns the new value
msg.map('ZZZ-1', (orig) => orig.toUpperCase())
// Renumber LAN segment ids (LAN-1)
msg.setIteration<string>('LAN-1', (_v, i) => i.toString())
// Iteration Map an iterated HL7 path using an array. Useful for renumbering segments by type.
msg.setIteration('LAN-1', ['A', 'B', 'C'])
Concatenating Transformers
The Transformer functions return the class itself allowing for the transformers to be chained in sequence.
msg
.transform({
restrict: {
MSH: () => true, // if function equates to true, keep whole segment
LAN: 3, // an integer keeps only the nth iteration of the segment (LAN[3])
ZZZ: { // an object keeps only certain fields of the segment
1: true, // true keeps the entire field as is (ZZZ-1)
2: [1, 5] // an array keeps only certain components of the field (ZZZ-2.1, ZZZ-2.5)
},
STF: {
2: [1, 4],
3: [], // an empty array keeps no components, same as if key was undefined
4: [2],
5: true,
10: 1, // an integer keeps only the nth repeition of the field
11: (f) => f?.[5] === 'O' // f is 0-indexed. This is keeping only the repetition that has STF-11.6 === 'O'
},
EDU: true, // if true, keep whole segment as is
// Any segments not specifically listed in a `restrict` object are removed.
},
remove: {
LAN: 2,
EDU: {
1: true, // deletes EDU-1
2: [3], // deletes EDU-2.3
3: (f) => {
if (Array.isArray(f) && typeof f[0] === 'string')
return f[0] > '19820000'
return false
}, // A function deletes when returns true. This is deleting EDU-3 when EDU-3.1 is greater than the year 1982
4: 2 // deletes EDU-4[2]
}
}
})
.addSegment('ZZZ|Engine|gofer')
.copy('ZZZ-2', 'MSH-3')
.set('ZZZ-1', 'Developer')
.set('ZZZ-2', 'amaster507')
.delete('LAN')
.addSegment('LAN|1|ESL^SPANISH^ISO639|1^READ^HL70403~1^EXCELLENT^HL70404|')
.addSegment('LAN|1|ESL^SPANISH^ISO639|2^WRITE^HL70403~2^GOOD^HL70404|')
.addSegment('LAN|1|FRE^FRENCH^ISO639|3^SPEAK^HL70403~3^FAIR^HL70404|')
.setIteration<string>('LAN-1', (_v, i) => i.toString())
.move('LAN[1]-3[2]', 'LAN[1]-4')
.move('LAN[2]-3[2]', 'LAN[2]-4')
.move('LAN[3]-3[2]', 'LAN[3]-4')
.map('LAN-4.2', {
EXCELLENT: 'BEST',
GOOD: 'BETTER',
FAIR: 'GOOD',
})
Sub-classes
This section is still a work in progress.
Seg
, Segs
& Seg[]
Seg
— returned when there is only a single Segment by nameSegs
— returned when there are multiple Segments by name if no iteration is defined
Msg.getSegment(name: string, iteration?: number) => Seg | Segs
Seg[]
returnedSeg
classes in an array.
These will be 0-based index.
Msg.getSegments(name: string) => Seg[]
Fld
, Flds
, & Fld[]
-
Fld
(Seg.getField(index: number, iteration?: number)
) -
Flds
-
Fld[]
(Seg.getFields(index: number)
) returnedFld
classes in an array.
These will be 0-based index.
Rep[]
- Rep[] (Fld.getRepetitions()) returned Rep classes in an array.
These will be 0-based index.
Cmp
, Cmps
, & Cmp[]
Cmp
— returned when there is a single componentFld.getComponent(index?: number)
—Flds.getComponent(index?: number)
—Rep.getComponent(index?: number)
—Cmps
(Field.getComponent(index?: number)
) — returned when there are multiple componentsCmp[]
(Field.getComponents(index: number)
) — returnedCmp
classes in an array.
NOTICE These will be 0-based index.
Sub
, Subs
, & MultiSubs
SubComponent
(Component.getSubComponent(index: number)
)
Each of the above subclasses exposes the following methods:
json(strict?: boolean)
—toString()
—
The Seg
class exposes an additional getName
method that returns the segment name string.