diff --git a/README.md b/README.md index 40cadc0..c843544 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ import { Server } from 'node-hl7-server' const server = new Server() const IB = server.createInbound({port: 3000}, async (req, res) => { - const messageReq = req.getMessage() - const messageRes = res.getAckMessage() - // do your code here + const messageReq = req.getMessage() + const messageRes = res.getAckMessage() + // do your code here }) ``` @@ -78,23 +78,23 @@ TLS: import { Server } from 'node-hl7-server' const server = new Server( - { - tls: - { - key: fs.readFileSync(path.join('certs/', 'server-key.pem')), // where your certs are - cert: fs.readFileSync(path.join('certs/', 'server-crt.pem')), // where your certs are - rejectUnauthorized: false - } - }) + { + tls: + { + key: fs.readFileSync(path.join('certs/', 'server-key.pem')), // where your certs are + cert: fs.readFileSync(path.join('certs/', 'server-crt.pem')), // where your certs are + rejectUnauthorized: false + } + }) ``` When you get a message, you can then parse any segment of the message and do you need to in order for your app to work. ```ts const IB_ADT = server.createInbound({port: LISTEN_PORT}, async (req, res) => { - const messageReq = req.getMessage() - const messageRes = res.getAckMessage() - const hl7Version = messageReq.get('MSH.12').toString() + const messageReq = req.getMessage() + const messageRes = res.getAckMessage() + const hl7Version = messageReq.get('MSH.12').toString() }) ``` @@ -103,21 +103,15 @@ Then you can query the segment ```MSH.12``` in this instance and get its result. Please consult [node-hl7-client](https://www.npmjs.com/package/node-hl7-client) documentation for further ways to parse the message segment. -### Override MSH Header +### Override response MSH fields -In HL7 specification 2.1 through 2.3.1, MSH header segment could not be 'ACK'. -It was a combination of ACK and MSH 9.2 (e.g. ADT_A01) for the full reference of ```ACK^A01^ADT_A01```. -From 2.4 specification to current, -a client might be looking for just ```ACK``` instead of ```ADT_A01``` given now the output of ```ACK^A01^ACK```. -This is not default behavior as the two pair results of ```ADT_A01``` give more information back -since MSH 9.1 is usually ACK to start with. +Individual response MSH segment fields can be overridden by passing the optional `mshOverrides` prop to `server.createInbound`. -To override this please setup your listener with: +For example, the following overrides the default MSH field 9.3 value to "ACK": ```ts - const listener = server.createInbound({port: 3000, overrideMSH: true }, async (req, res) => {}) + const listener = server.createInbound({ port: 3000, mshOverrides: { '9.3': 'ACK' }}, async (req, res) => {}) ``` -... and if the specification covers the MSH 9.3 as ACK, it will make it so. Otherwise, it will just be empty. ## Docker @@ -127,7 +121,7 @@ npm run docker:build This package, if you download from source, comes with a DockerFile to build a simple docker image with a basic node-hl7-server running. -All the server does is respond "success" to all properly formatted HL7 messages. +All the server does is respond "success" to all properly formatted HL7 messages. If you want more a custom instance of this server, download the GIT, and modify ```docker/server.js``` to your liking and then build the docker image and run it. diff --git a/__tests__/hl7.end2end.test.ts b/__tests__/hl7.end2end.test.ts index 5137c27..00d8464 100644 --- a/__tests__/hl7.end2end.test.ts +++ b/__tests__/hl7.end2end.test.ts @@ -154,18 +154,20 @@ describe('node hl7 end to end - client', () => { }) - test('...simple connect ... MSH 9.3 override', async () => { + test('...simple connect ... MSH overrides', async () => { let dfd = createDeferred() const server = new Server({bindAddress: '0.0.0.0'}) - const listener = server.createInbound({port: 3000, overrideMSH: true }, async (req, res) => { + // override MSH field 9.3 to "ACK" and field 18 to "UNICODE UTF-8" + const listener = server.createInbound({port: 3000, mshOverrides: { '9.3': 'ACK', '18': 'UNICODE UTF-8' } }, async (req, res) => { const messageReq = req.getMessage() expect(messageReq.get('MSH.12').toString()).toBe('2.7') await res.sendResponse('AA') const messageRes = res.getAckMessage() expect(messageRes?.get('MSA.1').toString()).toBe('AA') expect(messageRes?.get('MSH.9.3').toString()).toBe('ACK') + expect(messageRes?.get('MSH.18').toString()).toBe('UNICODE UTF-8') }) await expectEvent(listener, 'listen') @@ -334,15 +336,15 @@ describe('node hl7 end to end - client', () => { let dfd = createDeferred() const server = new Server( - { - bindAddress: '0.0.0.0', - tls: - { - key: fs.readFileSync(path.join('certs/', 'server-key.pem')), - cert: fs.readFileSync(path.join('certs/', 'server-crt.pem')), - rejectUnauthorized: false - } - }) + { + bindAddress: '0.0.0.0', + tls: + { + key: fs.readFileSync(path.join('certs/', 'server-key.pem')), + cert: fs.readFileSync(path.join('certs/', 'server-crt.pem')), + rejectUnauthorized: false + } + }) const inbound = server.createInbound({ port: 3000 }, async (req, res) => { const messageReq = req.getMessage() expect(messageReq.get('MSH.12').toString()).toBe('2.7') @@ -393,15 +395,15 @@ describe('node hl7 end to end - client', () => { const server = new Server({ bindAddress: "0.0.0.0" }); const listener = server.createInbound( - { port: 3000 }, - async (req, res) => { - const messageReq = req.getMessage(); - expect(messageReq.get("MSH.12").toString()).toBe("2.7"); - expect(messageReq.get("OBX.3.1").toString()).toBe("SOME-PDF"); - await res.sendResponse("AA"); - const messageRes = res.getAckMessage(); - expect(messageRes?.get("MSA.1").toString()).toBe("AA"); - } + { port: 3000 }, + async (req, res) => { + const messageReq = req.getMessage(); + expect(messageReq.get("MSH.12").toString()).toBe("2.7"); + expect(messageReq.get("OBX.3.1").toString()).toBe("SOME-PDF"); + await res.sendResponse("AA"); + const messageRes = res.getAckMessage(); + expect(messageRes?.get("MSA.1").toString()).toBe("AA"); + } ); await expectEvent(listener, "listen"); diff --git a/package.json b/package.json index 67b495d..da4d0fe 100644 --- a/package.json +++ b/package.json @@ -65,23 +65,23 @@ "@semantic-release/git": "^10.0.1", "@semantic-release/release-notes-generator": "^14.0.1", "@the-rabbit-hole/semantic-release-config": "^1.5.0", - "@types/node": "^22.4.0", + "@types/node": "^22.5.5", "@types/tcp-port-used": "^1.0.4", "@typescript-eslint/parser": "^8.1.0", - "@vitest/coverage-v8": "^2.0.5", - "@vitest/ui": "^2.0.5", + "@vitest/coverage-v8": "^2.1.1", + "@vitest/ui": "^2.1.1", "npm-package-json-lint": "^8.0.0", "portfinder": "^1.0.32", "pre-commit": "^1.2.2", - "semantic-release": "^24.0.0", + "semantic-release": "^24.1.1", "snazzy": "^9.0.0", "tcp-port-used": "^1.0.2", "ts-node": "^10.9.2", "ts-standard": "^12.0.2", - "tsd": "^0.31.1", - "typedoc": "^0.26.5", - "typescript": "5.5.4", - "vitest": "^2.0.5" + "tsd": "^0.31.2", + "typedoc": "^0.26.7", + "typescript": "5.6.2", + "vitest": "^2.1.1" }, "dependencies": { "node-hl7-client": "^2.3.1" diff --git a/src/server/inbound.ts b/src/server/inbound.ts index d03bc1f..8ed9237 100644 --- a/src/server/inbound.ts +++ b/src/server/inbound.ts @@ -180,7 +180,7 @@ export class Inbound extends EventEmitter implements Inbound { // create the inbound request const req = new InboundRequest(messageParsed, { type: 'file' }) // create the send response function - const res = new SendResponse(socket, message, this._opt.overrideMSH) + const res = new SendResponse(socket, message, this._opt.mshOverrides) // on a response sent, tell the inbound listener res.on('response.sent', () => { this.emit('response.sent') @@ -201,7 +201,7 @@ export class Inbound extends EventEmitter implements Inbound { // create the inbound request const req = new InboundRequest(messageParsed, { type: 'file' }) // create the send response function - const res = new SendResponse(socket, messageParsed, this._opt.overrideMSH) + const res = new SendResponse(socket, messageParsed, this._opt.mshOverrides) // on a response sent, tell the inbound listener void this._handler(req, res) }) @@ -213,7 +213,7 @@ export class Inbound extends EventEmitter implements Inbound { // create the inbound request const req = new InboundRequest(messageParsed, { type: 'file' }) // create the send response function - const res = new SendResponse(socket, messageParsed, this._opt.overrideMSH) + const res = new SendResponse(socket, messageParsed, this._opt.mshOverrides) // on a response sent, tell the inbound listener void this._handler(req, res) } diff --git a/src/server/modules/sendResponse.ts b/src/server/modules/sendResponse.ts index b8ca1ba..18c77a8 100644 --- a/src/server/modules/sendResponse.ts +++ b/src/server/modules/sendResponse.ts @@ -11,6 +11,7 @@ import { HL7_2_5_1, HL7_2_6, HL7_2_7, HL7_2_7_1, HL7_2_8 } from 'node-hl7-client/hl7' import { PROTOCOL_MLLP_FOOTER, PROTOCOL_MLLP_HEADER } from '../../utils/constants.js' +import type { ListenerOptions } from '../../utils/normalize.js' /** * Send Response @@ -24,14 +25,14 @@ export class SendResponse extends EventEmitter { /** @internal */ private readonly _message: Message /** @internal */ - private readonly _overrideMSH: boolean + private readonly _mshOverrides: ListenerOptions['mshOverrides'] - constructor (socket: Socket, message: Message, overrideMSH: boolean) { + constructor (socket: Socket, message: Message, mshOverrides?: ListenerOptions['mshOverrides']) { super() this._ack = undefined this._message = message + this._mshOverrides = mshOverrides this._socket = socket - this._overrideMSH = overrideMSH } /** @@ -140,7 +141,6 @@ export class SendResponse extends EventEmitter { messageHeader: { msh_9_1: 'ACK', msh_9_2: message.get('MSH.9.2').toString(), - msh_9_3: this._overrideMSH ? 'ACK' : undefined, msh_10: 'ACK', msh_11_1: message.get('MSH.11.1').toString() as 'P' | 'D' | 'T' } @@ -152,6 +152,12 @@ export class SendResponse extends EventEmitter { ackMessage.set('MSH.6', message.get('MSH.4').toString()) ackMessage.set('MSH.12', message.get('MSH.12').toString()) + if (typeof this._mshOverrides === 'object') { + Object.entries(this._mshOverrides).forEach(([path, value]) => { + ackMessage.set(`MSH.${path}`, value) + }) + } + const segment = ackMessage.addSegment('MSA') segment.set('1', type) segment.set('2', message.get('MSH.10').toString()) diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts index 2700012..f3f3077 100644 --- a/src/utils/normalize.ts +++ b/src/utils/normalize.ts @@ -11,7 +11,6 @@ const DEFAULT_SERVER_OPTS = { } const DEFAULT_LISTENER_OPTS = { - overrideMSH: false, encoding: 'utf-8' } @@ -39,12 +38,11 @@ export interface ServerOptions { * @since 1.0.0 */ export interface ListenerOptions { - /** The HL7 spec we are going to be creating - * Set this to true if the spec that you expect to be receiving from the client - * can support MSH 9.3 as type 'ACK'. - * Otherwise, MSH 9.3 will remain a combined string of ACK and MSH 9.2. - * @since 2.3.0 */ - overrideMSH?: boolean + /** Optional MSH segment overrides + * syntax: "field path as numbers separated by dots": "field value" + * e.g. { '9.3': 'ACK' } → MSH field 9.3 set to "ACK" + * @since 2.5.0 */ + mshOverrides?: Record /** Name of the Listener (e.g., IB_EPIC_ADT) * @default Randomized String */ name?: string @@ -61,17 +59,17 @@ export interface ListenerOptions { * @since 1.0.0 */ type ValidatedKeys = - | 'name' - | 'port' - | 'encoding' + | 'name' + | 'port' + | 'encoding' /** * @since 1.0.0 */ interface ValidatedOptions extends Pick, ValidatedKeys> { + mshOverrides?: Record name: string port: number - overrideMSH: boolean } /** @internal */