From 0f3769ede6da9a888e125334269d025aa21a0757 Mon Sep 17 00:00:00 2001 From: Frazer Smith Date: Sat, 20 May 2023 14:38:20 +0100 Subject: [PATCH] feat(routes): add hl7v2-to-json route --- README.md | 1 + package-lock.json | 15 +++++++ 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, 175 insertions(+) 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 3d24ba929..fa23b0769 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Docsmith is a RESTful API, built using Node.js and the [Fastify](https://fastify - DOC to TXT - DOCX to HTML - DOCX to TXT +- HL7v2 to JSON ("vertical bar" encoded) - HTML to TXT - PDF to HTML - PDF to TXT diff --git a/package-lock.json b/package-lock.json index 8ac4108a1..b094cf48b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@fastify/static": "^6.10.1", "@fastify/swagger": "^8.4.0", "@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", @@ -2338,6 +2339,15 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" }, + "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", @@ -3173,6 +3183,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 fee1e692d..c6f6c306d 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@fastify/static": "^6.10.1", "@fastify/swagger": "^8.4.0", "@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 e90d34285..6f889c93c 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -273,6 +273,11 @@ async function getConfig() { description: "Endpoints used for the conversion of DOCX documents", }, + { + name: "HL7v2", + description: + 'Endpoints used for the conversion of "vertical bar" encoded 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..ffc8b567f --- /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 + * "vertical bar" encoded 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.toString()); + req.conversionResults.body = results; + } catch (err) { + /** + * 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(err.message); + } + }); +} + +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..fbafa5aa3 --- /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( + "text/plain", + { parseAs: "string" }, + async (_req, payload) => { + /** + * The Content-Type header can be spoofed so is not trusted implicitly, + * this checks the payload is a "vertical bar" encoded 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..1e315b594 --- /dev/null +++ b/src/routes/hl7v2/json/schema.js @@ -0,0 +1,40 @@ +const S = require("fluent-json-schema"); + +const tags = ["HL7v2"]; + +/** + * 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 "vertical bar" encoded HL7 v2.x message to JSON', + description: + 'Returns the result of converting a "vertical bar" HL7 v2.x document to JSON format.', + operationId: "posthl7v2ToJson", + consumes: ["text/plain"], + 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 };