From 64a496eab73c4462bcaac0f18ff4353db9f2b5bc Mon Sep 17 00:00:00 2001 From: Frazer Smith Date: Tue, 30 May 2023 21:11:59 +0100 Subject: [PATCH] feat(routes): add hl7v2-to-json route --- README.md | 1 + package-lock.json | 17 +++++++- package.json | 1 + src/config/index.js | 5 +++ src/plugins/hl7v2-to-json/index.js | 44 +++++++++++++++++++ src/routes/hl7v2/json/index.js | 69 ++++++++++++++++++++++++++++++ src/routes/hl7v2/json/schema.js | 40 +++++++++++++++++ 7 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/plugins/hl7v2-to-json/index.js create mode 100644 src/routes/hl7v2/json/index.js create mode 100644 src/routes/hl7v2/json/schema.js diff --git a/README.md b/README.md index 05569765c..eb30d366c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Docsmith is a RESTful API, built using Node.js and the [Fastify](https://fastify | DOC | TXT | DOT file variant supported | | DOCX | HTML | DOCM, DOTM, and DOTX file variants supported | | DOCX | TXT | DOCM, DOTM, and DOTX file variants supported | +| HL7v2 | JSON | | | HTML | TXT | | | PDF | HTML | | | PDF | TXT | Scanned documents supported using OCR | diff --git a/package-lock.json b/package-lock.json index faea6f45a..d81d522c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "docsmith", - "version": "10.2.1", + "version": "10.3.0", "license": "MIT", "dependencies": { "@fastify/accepts": "^4.1.0", @@ -20,6 +20,7 @@ "@fastify/static": "^6.10.2", "@fastify/swagger": "^8.5.1", "@fastify/under-pressure": "^8.2.0", + "@redoxengine/redox-hl7-v2": "^1.0.1", "cfb": "^1.2.2", "clean-css": "^5.3.2", "cssesc": "^3.0.0", @@ -2360,6 +2361,15 @@ "node": ">=10" } }, + "node_modules/@redoxengine/redox-hl7-v2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@redoxengine/redox-hl7-v2/-/redox-hl7-v2-1.0.1.tgz", + "integrity": "sha512-o6bgOpoK0J+NL6DF5Db8+2MIXgqPdHU6pBpWLjxiKuoCpq+dTFzaf3SJ0a/IExeGvzFaLNOnd8c5QAL7OdsQ5w==", + "dependencies": { + "async": "^1.2.1", + "lodash": "^4.17.5" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -3203,6 +3213,11 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index 694e7105f..45deb8efd 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@fastify/static": "^6.10.2", "@fastify/swagger": "^8.5.1", "@fastify/under-pressure": "^8.2.0", + "@redoxengine/redox-hl7-v2": "^1.0.1", "cfb": "^1.2.2", "clean-css": "^5.3.2", "cssesc": "^3.0.0", diff --git a/src/config/index.js b/src/config/index.js index 171361f41..3ce9ffe12 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -293,6 +293,11 @@ async function getConfig() { description: "Endpoints used for the conversion of DOTX documents", }, + { + name: "HL7v2", + description: + "Endpoints used for the conversion of HL7 v2.x messages", + }, { name: "HTML", description: diff --git a/src/plugins/hl7v2-to-json/index.js b/src/plugins/hl7v2-to-json/index.js new file mode 100644 index 000000000..2cebea8cf --- /dev/null +++ b/src/plugins/hl7v2-to-json/index.js @@ -0,0 +1,44 @@ +const fp = require("fastify-plugin"); +const hl7v2 = require("@redoxengine/redox-hl7-v2"); + +/** + * @author Frazer Smith + * @description Pre-handler plugin that uses redox-hl7-v2 to convert string containing + * HL7 v2.x in `req.body` to JSON. + * `req` object is decorated with `conversionResults.body` holding the converted document. + * @param {object} server - Fastify instance. + */ +async function plugin(server) { + const parser = new hl7v2.Parser(); + + server.addHook("onRequest", async (req) => { + req.conversionResults = { body: undefined }; + }); + + server.addHook("preHandler", async (req) => { + /** + * `htmlToText` function still attempts to parse empty bodies/input or invalid HTML + * and produces results, so catch them here + */ + // if (req.body === undefined || Object.keys(req.body).length === 0) { + // throw server.httpErrors.badRequest(); + // } + + try { + const results = parser.parse(req.body); + req.conversionResults.body = results; + } catch { + /** + * redox-hl7-v2 will throw if the HL7 v2 message provided + * by client is malformed or invalid, thus client error code + */ + throw server.httpErrors.badRequest(); + } + }); +} + +module.exports = fp(plugin, { + fastify: "4.x", + name: "hl7v2ToJson", + dependencies: ["@fastify/sensible"], +}); diff --git a/src/routes/hl7v2/json/index.js b/src/routes/hl7v2/json/index.js new file mode 100644 index 000000000..42cd25967 --- /dev/null +++ b/src/routes/hl7v2/json/index.js @@ -0,0 +1,69 @@ +// Import plugins +const cors = require("@fastify/cors"); +const hl7v2ToJson = require("../../../plugins/hl7v2-to-json"); + +const { hl7v2ToJsonPostSchema } = require("./schema"); + +const accepts = hl7v2ToJsonPostSchema.produces; + +/** + * @author Frazer Smith + * @description Sets routing options for server. + * @param {object} server - Fastify instance. + * @param {object} options - Route config values. + * @param {*=} options.bearerTokenAuthKeys - Apply `bearerToken` security scheme to route if defined. + * @param {object} options.cors - CORS settings. + */ +async function route(server, options) { + if (options.bearerTokenAuthKeys) { + hl7v2ToJsonPostSchema.security = [{ bearerToken: [] }]; + hl7v2ToJsonPostSchema.response[401] = { + $ref: "responses#/properties/unauthorized", + description: "Unauthorized", + }; + } + + server.addContentTypeParser( + hl7v2ToJsonPostSchema.consumes, + { parseAs: "string" }, + async (_req, payload) => { + /** + * The Content-Type header can be spoofed so is not trusted implicitly, + * this checks the payload is an HL7 v2.x message + */ + if (!payload.startsWith("MSH")) { + throw server.httpErrors.unsupportedMediaType(); + } + + return payload; + } + ); + + // Register plugins + await server + // Enable CORS if options passed + .register(cors, { + ...options.cors, + methods: ["POST"], + }) + .register(hl7v2ToJson); + + server.route({ + method: "POST", + url: "/", + schema: hl7v2ToJsonPostSchema, + onRequest: async (req) => { + if ( + // Catch unsupported Accept header media types + !req.accepts().type(accepts) + ) { + throw server.httpErrors.notAcceptable(); + } + }, + handler: (req, res) => { + res.send(req.conversionResults.body); + }, + }); +} + +module.exports = route; diff --git a/src/routes/hl7v2/json/schema.js b/src/routes/hl7v2/json/schema.js new file mode 100644 index 000000000..60594f0c1 --- /dev/null +++ b/src/routes/hl7v2/json/schema.js @@ -0,0 +1,40 @@ +const S = require("fluent-json-schema"); + +const tags = ["HL7 v2.x"]; + +/** + * Fastify uses AJV for JSON Schema Validation, + * see https://fastify.io/docs/latest/Reference/Validation-and-Serialization/ + * + * Input validation protects against XSS, HPP, prototype pollution, + * and most other injection attacks. + */ +const hl7v2ToJsonPostSchema = { + tags, + summary: "Convert HL7 v2.x message to JSON", + description: + "Returns the result of converting an HL7 v2.x message to JSON format.", + operationId: "postHl7v2ToJson", + consumes: ["text/hl7v2"], + produces: ["application/json"], + response: { + 200: S.object().additionalProperties(true), + 400: S.ref("responses#/properties/badRequest").description( + "Bad Request" + ), + 406: S.ref("responses#/properties/notAcceptable").description( + "Not Acceptable" + ), + 415: S.ref("responses#/properties/unsupportedMediaType").description( + "Unsupported Media Type" + ), + 429: S.ref("responses#/properties/tooManyRequests").description( + "Too Many Requests" + ), + 503: S.ref("responses#/properties/serviceUnavailable").description( + "Service Unavailable" + ), + }, +}; + +module.exports = { hl7v2ToJsonPostSchema };