├── .npmignore ├── .travis.yml ├── .jshintrc ├── bower.json ├── lib ├── contentModifier.js ├── xmlutil.js ├── condition.js ├── entryLevel │ ├── socialHistoryEntryLevel.js │ ├── encounterEntryLevel.js │ ├── index.js │ ├── vitalSignEntryLevel.js │ ├── resultEntryLevel.js │ ├── payerEntryLevel.js │ ├── allergyEntryLevel.js │ ├── immunizationEntryLevel.js │ ├── problemEntryLevel.js │ ├── planOfCareEntryLevel.js │ ├── sharedEntryLevel.js │ ├── procedureEntryLevel.js │ └── medicationEntryLevel.js ├── leafLevel.js ├── translate.js ├── engine.js ├── fieldLevel.js ├── documentLevel.js ├── headerLevel.js ├── sectionLevel2.js └── htmlHeaders.js ├── .jsbeautifyrc ├── Jenkinsfile ├── test ├── xmlmods │ ├── bbGenerator.js │ ├── ccd1Generator.js │ ├── bbParser.js │ ├── viteraGenerator.js │ ├── ccd1Parser.js │ ├── templatePath.js │ └── viteraParser.js ├── util │ ├── index.js │ ├── jsonutil.js │ ├── xml2jsutil.js │ └── xpathutil.js ├── sample_runs │ ├── test-sample.js │ ├── test-xml-vs-generatedxml.js │ └── test-gen-parse-gen.js └── unit │ └── test-translate.js ├── .gitignore ├── browser └── lib │ └── xmlutil.js ├── RELEASENOTES.md ├── package.json ├── index.js ├── docs └── old_generator.md ├── Gruntfile.js ├── README.md ├── jest.config.js └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "14.19.3" 5 | 6 | before_script: 7 | - npm install -g grunt-cli 8 | - npm install --quiet 9 | 10 | script: 11 | - grunt test 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": false, // Standard browser globals e.g. `window`, `document`. 3 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 4 | "indent": 2 // Specify indentation spacing 5 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blue-button-generate", 3 | "version": "1.5.0-beta.3", 4 | "license": "Apache-2.0", 5 | "ignore": ["**/*", "!dist/*", "!bower.json", "!package.json", "!LICENSE"], 6 | "devDependencies": { 7 | "blue-button": "^1.5.0-beta.5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/contentModifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.key = function (overrideKeyValue) { 4 | return function (template) { 5 | template.key = overrideKeyValue; 6 | }; 7 | }; 8 | 9 | exports.required = function (template) { 10 | template.required = true; 11 | }; 12 | 13 | exports.dataKey = function (overrideKeyValue) { 14 | return function (template) { 15 | template.dataKey = overrideKeyValue; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_size": 2, 3 | "indent_char": " ", 4 | "indent_level": 0, 5 | "indent_with_tabs": false, 6 | "preserve_newlines": true, 7 | "max_preserve_newlines": 2, 8 | "jslint_happy": true, 9 | "brace_style": "collapse", 10 | "keep_array_indentation": false, 11 | "keep_function_indentation": false, 12 | "space_before_conditional": true, 13 | "break_chained_methods": false, 14 | "eval_code": false, 15 | "unescape_strings": false, 16 | "wrap_line_length": 0 17 | } -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'node:18.20.1-alpine3.19' 5 | } 6 | } 7 | stages { 8 | stage('Sanity Check') { 9 | steps { 10 | echo 'Node installation check...' 11 | sh 'node --version' 12 | } 13 | } 14 | stage('Build') { 15 | steps { 16 | echo 'Installing dependencies...' 17 | sh 'npm i' 18 | } 19 | } 20 | stage('Test') { 21 | steps { 22 | echo 'Testing...' 23 | sh 'npm run coverage' 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /lib/xmlutil.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var libxmljs = require("libxmljs"); 4 | 5 | exports.newDocument = function () { 6 | return new libxmljs.Document(); 7 | }; 8 | 9 | exports.newNode = function (xmlDoc, name, text) { 10 | if ((text === undefined) || (text === null)) { 11 | return xmlDoc.node(name); 12 | } else { 13 | return xmlDoc.node(name, text); 14 | } 15 | }; 16 | 17 | exports.nodeAttr = function (node, attr) { 18 | node.attr(attr); 19 | }; 20 | 21 | exports.serializeToString = function (xmlDoc) { 22 | return xmlDoc.toString(); 23 | }; 24 | -------------------------------------------------------------------------------- /test/xmlmods/bbGenerator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | module.exports = [{ 6 | xpath: "//*[@nullFlavor]", 7 | action: "removeNode" 8 | }, { 9 | xpath: "//h:text", 10 | action: "removeNode" 11 | }, { 12 | xpath: "//h:originalText", 13 | action: "removeNode" 14 | }, { 15 | xpath: t.payersSection + '/.//h:time', 16 | action: "removeNode" 17 | }, { 18 | xpath: t.vitalsSection + '/..', 19 | action: "flatten", 20 | params: "2.16.840.1.113883.10.20.22.4.27" 21 | }, { 22 | xpath: t.vitalsSection + '/h:entry', 23 | action: 'removeNode' 24 | }, { 25 | xpath: "//h:effectiveTime[@value] | //h:effectiveTime/h:low[@value] | //h:effectiveTime/h:high[@value]", 26 | action: "removeTimezone", 27 | comment: "parser bug: timezones are not read" 28 | }]; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | bower_components 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | # Generated Files For Tests 33 | test/fixtures/files/generated/ 34 | 35 | resources 36 | -------------------------------------------------------------------------------- /lib/condition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.keyExists = function (key) { 4 | return function (input) { 5 | return input.hasOwnProperty(key); 6 | }; 7 | }; 8 | 9 | exports.keyDoesntExist = function (key) { 10 | return function (input) { 11 | return !input.hasOwnProperty(key); 12 | }; 13 | }; 14 | 15 | exports.eitherKeyExists = function (key0, key1, key2, key3) { 16 | return function (input) { 17 | return input.hasOwnProperty(key0) || input.hasOwnProperty(key1) || input.hasOwnProperty(key2) || input.hasOwnProperty(key3); 18 | }; 19 | }; 20 | 21 | exports.codeOrDisplayname = function (input) { 22 | return input.code || input.name; 23 | }; 24 | 25 | exports.propertyEquals = function (property, value) { 26 | return function (input) { 27 | return input && (input[property] === value); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /test/xmlmods/ccd1Generator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | var normalizedCodeSystemNames = { 6 | "SNOMED-CT": "SNOMED CT" // codes available from blue-button-meta should be consinstent with normalization, fix in blue-button-meta 7 | }; 8 | 9 | module.exports = [{ 10 | xpath: t.medSection + '/.//h:effectiveTime[@xsi:type="IVL_TS"]', 11 | action: "removeAttribute", 12 | params: "type" 13 | }, { 14 | xpath: t.immSection + '/.//h:effectiveTime[@xsi:type="IVL_TS"]', 15 | action: "removeAttribute", 16 | params: "type" 17 | }, { 18 | xpath: "//*[@codeSystem][@codeSystemName]", 19 | action: "normalize", 20 | params: { 21 | attr: "codeSystemName", 22 | srcAttr: "codeSystem", 23 | map: normalizedCodeSystemNames 24 | }, 25 | comment: 'needs to be fixed in blue-button-meta' 26 | }]; 27 | -------------------------------------------------------------------------------- /test/util/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var xpathutil = require('./xpathutil'); 4 | var xml2jsutil = require('./xml2jsutil'); 5 | 6 | var bb = require('@amida-tech/blue-button'); 7 | var bbg = require('../../index'); 8 | 9 | exports.toSectionJSONs = function (xml, mods, callback) { 10 | if (mods) { 11 | xml = xpathutil.modifyXML(xml, mods); 12 | } 13 | xml2jsutil.toOrderedSectionJSONs(xml, callback); 14 | }; 15 | 16 | exports.toBBSectionJSONs = function (xml, validate, mods, callback) { 17 | var bbJSON = bb.parseString(xml); 18 | if (validate) { 19 | var val = bb.validator.validateDocumentModel(bbJSON); 20 | if (!val) { 21 | callback(new Error("Validation failed.")); 22 | return; 23 | } 24 | } 25 | 26 | var xmlGenerated = bbg.generateCCD(bbJSON); 27 | if (mods) { 28 | xmlGenerated = xpathutil.modifyXML(xmlGenerated, mods); 29 | } 30 | xml2jsutil.toOrderedSectionJSONs(xmlGenerated, callback); 31 | }; 32 | -------------------------------------------------------------------------------- /browser/lib/xmlutil.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.newDocument = function () { 4 | return document.implementation.createDocument("", "", null); 5 | }; 6 | 7 | exports.newNode = function (xmlDoc, name, text) { 8 | var doc = xmlDoc.ownerDocument || xmlDoc; 9 | 10 | var element = doc.createElement(name); 11 | if ((text !== undefined) && (text !== null)) { 12 | var textNode = doc.createTextNode(text); 13 | element.appendChild(textNode); 14 | } 15 | if (xmlDoc.ownerDocument) { 16 | xmlDoc.appendChild(element); 17 | } else { 18 | xmlDoc.appendChild(element); 19 | } 20 | return element; 21 | }; 22 | 23 | exports.nodeAttr = function (node, attr) { 24 | Object.keys(attr).forEach(function(key) { 25 | var value = attr[key]; 26 | node.setAttribute(key, value); 27 | }); 28 | }; 29 | 30 | exports.serializeToString = function (xmlDoc) { 31 | var serializer = new XMLSerializer(); 32 | var result = serializer.serializeToString(xmlDoc); 33 | return result; 34 | }; 35 | -------------------------------------------------------------------------------- /test/sample_runs/test-sample.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require('path'); 3 | var bb = require('@amida-tech/blue-button'); 4 | var bbg = require('../../index'); 5 | 6 | describe('parse generate parse generate', function () { 7 | var generatedDir = null; 8 | 9 | beforeAll(function () { 10 | generatedDir = path.join(__dirname, "../fixtures/files/generated"); 11 | expect(generatedDir).toBeDefined(); 12 | }); 13 | 14 | it('verifying basic generation still works', function () { 15 | var data = fs.readFileSync(__dirname + "/../fixtures/files/ccda_xml/CCD_1.xml").toString(); 16 | var result = bb.parseString(data); 17 | 18 | // check validation 19 | var val = bb.validator.validateDocumentModel(result); 20 | expect(val).toBe(true); 21 | 22 | // generate ccda 23 | var xml = bbg.generateCCD(result); 24 | 25 | fs.writeFileSync(generatedDir + '/CCD_1.xml', xml); 26 | 27 | expect(xml).toBeTruthy(); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | # blue-button-generate Release Notes 2 | 3 | ## v.1.5.8 - July 13, 2022 4 | - Patch update: 1.5.8: Upgraded dependencies 5 | 6 | ## v.1.5.0 - June 14, 2015 7 | - Separate race and ethnicity from blue-button is now supported. 8 | - Support for input data only is removed. Your input now must have both data and meta components. 9 | - Set Identifier and Confidentiality Code are now read from CCDA JSON meta properties. 10 | - Add preventNullFlavor option to generateCCD 11 | - Refactored humand readable section generation (HTML) 12 | 13 | ## v.1.4.0 - March 25, 2105 14 | - Results of xsi:type ST are now supported 15 | - Medication supply organization is now supported 16 | - Text fields are added for results, encounters, medications, allergies 17 | - Providers are added to the CCDA header 18 | - Each entry can be given a unique id based on an option 19 | 20 | ## v.1.3.0 - December 12, 2014 21 | - Seperated from blue-button repository.' 22 | - Rewritten using a new infrastructure with JSON templates. 23 | - Full compare tests with source xml and generated xml are added. 24 | - Browser support using browserify. 25 | - Added to bower. 26 | - Bug fixes. 27 | -------------------------------------------------------------------------------- /test/unit/test-translate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var translate = require('../../lib/translate'); 4 | 5 | describe("time generation", function () { 6 | var testCases = [{ 7 | hl7: '2012', 8 | date: "2012-01-01T00:00:00.000Z", 9 | precision: 'year' 10 | }, { 11 | hl7: '201209', 12 | date: "2012-09-01T00:00:00.000Z", 13 | precision: 'month' 14 | }, { 15 | hl7: '20120915', 16 | date: "2012-09-15T00:00:00.000Z", 17 | precision: 'day' 18 | }, { 19 | hl7: '20120915', 20 | date: "2012-09-15T00:00:00.000Z", 21 | precision: 'day' 22 | }, { 23 | hl7: '20120915', 24 | date: "2012-09-15T00:00:00.000Z", 25 | precision: 'day' 26 | }, { 27 | hl7: '20120915', 28 | date: "2012-09-15T00:00:00.000Z", 29 | precision: 'day' 30 | }, { 31 | hl7: '20120915191442+0000', 32 | date: "2012-09-15T19:14:42.000Z", 33 | precision: 'second' 34 | }, { 35 | hl7: '20120915', 36 | date: "2012-09-15T00:00:00.000Z", 37 | precision: 'day' 38 | }, { 39 | hl7: '20120916021442.123+0000', 40 | date: "2012-09-16T02:14:42.123Z", 41 | precision: 'subsecond' 42 | }]; 43 | 44 | testCases.forEach(function (testCase) { 45 | var description = testCase.date + " (" + testCase.precision + ")"; 46 | it(description, function () { 47 | var input = { 48 | date: testCase.date, 49 | precision: testCase.precision 50 | }; 51 | var hl7 = translate.time(input); 52 | expect(hl7).toBe(testCase.hl7); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amida-tech/blue-button-generate", 3 | "version": "1.5.10", 4 | "description": "Blue Button CCDA Generator.", 5 | "main": "./index.js", 6 | "browser": { 7 | "./lib/xmlutil.js": "./browser/lib/xmlutil.js" 8 | }, 9 | "directories": { 10 | "doc": "doc", 11 | "lib": "lib" 12 | }, 13 | "scripts": { 14 | "test": "grunt test", 15 | "coverage": "grunt coverage" 16 | }, 17 | "author": "Dmitry Kachaev ", 18 | "contributors": [ 19 | { 20 | "name": "Matthew McCall", 21 | "email": "matt@amida-tech.com" 22 | }, 23 | { 24 | "name": "Afsin Ustundag", 25 | "email": "afsin.ustundag@us.pwc.com" 26 | }, 27 | { 28 | "name": "Matt Martz", 29 | "email": "matt.martz@amida-tech.com" 30 | } 31 | ], 32 | "license": "Apache-2.0", 33 | "engines": { 34 | "node": ">= 14.19.3" 35 | }, 36 | "dependencies": { 37 | "@amida-tech/blue-button-meta": "^1.7.5", 38 | "@amida-tech/blue-button-util": "^1.6.5", 39 | "libxmljs": "^1.0.11", 40 | "lodash": "^4.17.21", 41 | "moment": "^2.30.1", 42 | "uuid": "^10.0.0" 43 | }, 44 | "devDependencies": { 45 | "@amida-tech/blue-button": "^1.10.11", 46 | "brfs": "^2.0.2", 47 | "grunt": "^1.6.1", 48 | "grunt-contrib-connect": "^4.0.0", 49 | "grunt-contrib-jshint": "^3.2.0", 50 | "grunt-contrib-watch": "^1.1.0", 51 | "grunt-coveralls": "^2.0.0", 52 | "grunt-express-server": "^0.5.4", 53 | "grunt-jsbeautifier": "^0.2.13", 54 | "grunt-run": "^0.8.1", 55 | "jest": "^29.7.0", 56 | "xml2js": "^0.6.2" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/amida-tech/blue-button-generate.git" 61 | }, 62 | "keywords": [ 63 | "bluebutton", 64 | "ccda" 65 | ], 66 | "bugs": { 67 | "url": "https://github.com/amida-tech/blue-button-generate/issues" 68 | }, 69 | "homepage": "https://github.com/amida-tech/blue-button-generate" 70 | } 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | This script converts CCDA data in JSON format (originally generated from a Continuity of Care Document (CCD) in 5 | standard XML/CCDA format) back to XML/CCDA format. 6 | */ 7 | 8 | var _ = require('lodash'); 9 | var bbu = require("@amida-tech/blue-button-util"); 10 | 11 | var engine = require('./lib/engine'); 12 | var documentLevel = require('./lib/documentLevel'); 13 | 14 | var bbuo = bbu.object; 15 | 16 | var html_renderer = require('./lib/htmlHeaders'); 17 | 18 | var createContext = (function () { 19 | var base = { 20 | nextReference: function (referenceKey) { 21 | var index = this.references[referenceKey] || 0; 22 | ++index; 23 | this.references[referenceKey] = index; 24 | return "#" + referenceKey + index; 25 | }, 26 | sameReference: function (referenceKey) { 27 | var index = this.references[referenceKey] || 0; 28 | return "#" + referenceKey + index; 29 | } 30 | }; 31 | 32 | return function (options) { 33 | var result = Object.create(base); 34 | result.references = {}; 35 | if (options.meta && options.addUniqueIds) { 36 | result.rootId = _.get(options.meta, 'identifiers.0.identifier'); 37 | } else { 38 | result.rootId = null; 39 | } 40 | result.preventNullFlavor = options.preventNullFlavor; 41 | 42 | return result; 43 | }; 44 | })(); 45 | 46 | var generate = exports.generate = function (template, input, options) { 47 | var context = createContext(options); 48 | return engine.create(template, input, context); 49 | }; 50 | 51 | exports.generateCCD = function (input, options) { 52 | options = options || {}; 53 | options.meta = input.meta; 54 | if (!options.html_renderer) { 55 | options.html_renderer = html_renderer; 56 | } 57 | const template = documentLevel.ccd2(options.html_renderer) || documentLevel.ccd; 58 | return generate(template, input, options); 59 | }; 60 | 61 | exports.fieldLevel = require("./lib/fieldLevel"); 62 | exports.entryLevel = require("./lib/entryLevel"); 63 | exports.leafLevel = require('./lib/leafLevel'); 64 | exports.contentModifier = require("./lib/contentModifier"); 65 | exports.condition = require('./lib/condition'); 66 | -------------------------------------------------------------------------------- /lib/entryLevel/socialHistoryEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var required = contentModifier.required; 9 | 10 | exports.socialHistoryObservation = { 11 | key: "observation", 12 | attributes: { 13 | classCode: "OBS", 14 | moodCode: "EVN" 15 | }, 16 | content: [ 17 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.38"), 18 | fieldLevel.uniqueId, 19 | fieldLevel.id, { 20 | key: "code", 21 | attributes: leafLevel.code, 22 | content: [{ 23 | key: "originalText", 24 | text: leafLevel.inputProperty("unencoded_name"), 25 | content: { 26 | key: "reference", 27 | attributes: { 28 | "value": leafLevel.nextReference("social") 29 | } 30 | } 31 | }, { 32 | key: "translation", 33 | attributes: leafLevel.code, 34 | dataKey: "translations" 35 | }], 36 | dataKey: "code", 37 | }, 38 | fieldLevel.statusCodeCompleted, 39 | fieldLevel.effectiveTime, { 40 | key: "value", 41 | attributes: { 42 | "xsi:type": "ST" 43 | }, 44 | text: leafLevel.inputProperty("value") 45 | } 46 | ], 47 | existsWhen: function (input) { 48 | return (!input.value) || input.value.indexOf("smoke") < 0; 49 | } 50 | }; 51 | 52 | exports.smokingStatusObservation = { 53 | key: "observation", 54 | attributes: { 55 | classCode: "OBS", 56 | moodCode: "EVN" 57 | }, 58 | content: [ 59 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.78"), 60 | fieldLevel.uniqueId, 61 | fieldLevel.id, 62 | fieldLevel.templateCode("SmokingStatusObservation"), 63 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 64 | key: "value", 65 | attributes: [{ 66 | "xsi:type": "CD" 67 | }, 68 | leafLevel.codeFromName("2.16.840.1.113883.11.20.9.38") 69 | ], 70 | required: true, 71 | dataKey: "value" 72 | } 73 | ], 74 | existsWhen: function (input) { 75 | return input.value && input.value.indexOf("smoke") > -1; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /lib/entryLevel/encounterEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var contentModifier = require("../contentModifier"); 6 | 7 | var sharedEntryLevel = require("./sharedEntryLevel"); 8 | 9 | var key = contentModifier.key; 10 | var required = contentModifier.required; 11 | var dataKey = contentModifier.dataKey; 12 | 13 | exports.encounterActivities = { 14 | key: "encounter", 15 | attributes: { 16 | classCode: "ENC", 17 | moodCode: "EVN" 18 | }, 19 | content: [ 20 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.49"), 21 | fieldLevel.uniqueId, 22 | fieldLevel.id, { 23 | key: "code", 24 | attributes: leafLevel.code, 25 | content: [{ 26 | key: "originalText", 27 | content: [{ 28 | key: "reference", 29 | attributes: { 30 | "value": leafLevel.nextReference("Encounter") 31 | } 32 | }] 33 | }, { 34 | key: "translation", 35 | attributes: leafLevel.code, 36 | dataKey: "translations" 37 | }], 38 | dataKey: "encounter" 39 | }, 40 | [fieldLevel.effectiveTime, required], 41 | [fieldLevel.performer, dataKey("performers")], { 42 | key: "participant", 43 | attributes: { 44 | typeCode: "LOC" 45 | }, 46 | content: [ 47 | [sharedEntryLevel.serviceDeliveryLocation, required] 48 | ], 49 | dataKey: "locations" 50 | }, { 51 | key: "entryRelationship", 52 | attributes: { 53 | typeCode: "RSON" 54 | }, 55 | content: [ 56 | [sharedEntryLevel.indication, required] 57 | ], 58 | dataKey: "findings", 59 | dataTransform: function (input) { 60 | input = input.map(function (e) { 61 | e.code = { 62 | code: "404684003", 63 | name: "Finding", 64 | code_system: "2.16.840.1.113883.6.96", 65 | code_system_name: "SNOMED CT" 66 | }; 67 | return e; 68 | }); 69 | return input; 70 | }, 71 | toDo: "move dataTransform to blue-button-meta" 72 | } 73 | ], 74 | notImplemented: [ 75 | "entryRelationship:encounterDiagnosis", 76 | "dishargeDispositionCode" 77 | ] 78 | }; 79 | -------------------------------------------------------------------------------- /lib/entryLevel/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var allergyEntryLevel = require("./allergyEntryLevel"); 4 | var resultEntryLevel = require("./resultEntryLevel"); 5 | var socialHistoryEntryLevel = require('./socialHistoryEntryLevel'); 6 | var payerEntryLevel = require('./payerEntryLevel'); 7 | var vitalSignEntryLevel = require('./vitalSignEntryLevel'); 8 | var planOfCareEntryLevel = require('./planOfCareEntryLevel'); 9 | var procedureEntryLevel = require("./procedureEntryLevel"); 10 | var problemEntryLevel = require("./problemEntryLevel"); 11 | var encounterEntryLevel = require("./encounterEntryLevel"); 12 | var immunizationEntryLevel = require("./immunizationEntryLevel"); 13 | var medicationEntryLevel = require("./medicationEntryLevel"); 14 | 15 | exports.allergyProblemAct = allergyEntryLevel.allergyProblemAct; 16 | 17 | exports.medicationActivity = medicationEntryLevel.medicationActivity; 18 | 19 | exports.immunizationActivity = immunizationEntryLevel.immunizationActivity; 20 | 21 | exports.problemConcernAct = problemEntryLevel.problemConcernAct; 22 | 23 | exports.encounterActivities = encounterEntryLevel.encounterActivities; 24 | 25 | exports.procedureActivityAct = procedureEntryLevel.procedureActivityAct; 26 | exports.procedureActivityProcedure = procedureEntryLevel.procedureActivityProcedure; 27 | exports.procedureActivityObservation = procedureEntryLevel.procedureActivityObservation; 28 | 29 | exports.planOfCareActivityAct = planOfCareEntryLevel.planOfCareActivityAct; 30 | exports.planOfCareActivityObservation = planOfCareEntryLevel.planOfCareActivityObservation; 31 | exports.planOfCareActivityProcedure = planOfCareEntryLevel.planOfCareActivityProcedure; 32 | exports.planOfCareActivityEncounter = planOfCareEntryLevel.planOfCareActivityEncounter; 33 | exports.planOfCareActivitySubstanceAdministration = planOfCareEntryLevel.planOfCareActivitySubstanceAdministration; 34 | exports.planOfCareActivitySupply = planOfCareEntryLevel.planOfCareActivitySupply; 35 | exports.planOfCareActivityInstructions = planOfCareEntryLevel.planOfCareActivityInstructions; 36 | 37 | exports.coverageActivity = payerEntryLevel.coverageActivity; 38 | 39 | exports.vitalSignsOrganizer = vitalSignEntryLevel.vitalSignsOrganizer; 40 | 41 | exports.resultOrganizer = resultEntryLevel.resultOrganizer; 42 | 43 | exports.socialHistoryObservation = socialHistoryEntryLevel.socialHistoryObservation; 44 | exports.smokingStatusObservation = socialHistoryEntryLevel.smokingStatusObservation; 45 | -------------------------------------------------------------------------------- /lib/entryLevel/vitalSignEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | 7 | var contentModifier = require("../contentModifier"); 8 | 9 | var required = contentModifier.required; 10 | 11 | var vitalSignObservation = { 12 | key: "observation", 13 | attributes: { 14 | classCode: "OBS", 15 | moodCode: "EVN" 16 | }, 17 | content: [ 18 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.27"), 19 | fieldLevel.id, { 20 | key: "code", 21 | attributes: leafLevel.code, 22 | content: [{ 23 | key: "originalText", 24 | content: { 25 | key: "reference", 26 | attributes: { 27 | "value": leafLevel.nextReference("vital") 28 | } 29 | } 30 | }, { 31 | key: "translation", 32 | attributes: leafLevel.code, 33 | dataKey: "translations" 34 | }], 35 | dataKey: "vital", 36 | required: true 37 | }, { 38 | key: "statusCode", 39 | attributes: { 40 | code: leafLevel.inputProperty("status") 41 | } 42 | }, 43 | [fieldLevel.effectiveTime, required], { 44 | key: "value", 45 | attributes: { 46 | "xsi:type": "PQ", 47 | value: leafLevel.inputProperty("value"), 48 | unit: leafLevel.inputProperty("unit") 49 | }, 50 | existsWhen: condition.keyExists("value"), 51 | required: true 52 | }, { 53 | key: "interpretationCode", 54 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.83"), 55 | dataKey: "interpretations" 56 | } 57 | ], 58 | notImplemented: [ 59 | "constant statusCode", 60 | "methodCode", 61 | "targetSiteCode", 62 | "author" 63 | ] 64 | }; 65 | 66 | exports.vitalSignsOrganizer = { 67 | key: "organizer", 68 | attributes: { 69 | classCode: "CLUSTER", 70 | moodCode: "EVN" 71 | }, 72 | content: [ 73 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.26"), 74 | fieldLevel.uniqueId, 75 | fieldLevel.id, 76 | fieldLevel.templateCode("VitalSignsOrganizer"), { 77 | key: "statusCode", 78 | attributes: { 79 | code: leafLevel.inputProperty("status") 80 | } 81 | }, 82 | [fieldLevel.effectiveTime, required], { 83 | key: "component", 84 | content: vitalSignObservation, 85 | required: true 86 | } 87 | ], 88 | notImplemented: [ 89 | "constant statusCode" 90 | ] 91 | }; 92 | -------------------------------------------------------------------------------- /lib/leafLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var bbu = require("@amida-tech/blue-button-util"); 4 | 5 | var translate = require('./translate'); 6 | 7 | var bbuo = bbu.object; 8 | var bbud = bbu.datetime; 9 | 10 | exports.input = function (input) { 11 | return input; 12 | }; 13 | 14 | exports.inputProperty = function (key) { 15 | return function (input) { 16 | return input && input[key]; 17 | }; 18 | }; 19 | 20 | exports.boolInputProperty = function (key) { 21 | return function (input) { 22 | if (input && input.hasOwnProperty(key)) { 23 | return input[key].toString(); 24 | } else { 25 | return null; 26 | } 27 | }; 28 | }; 29 | 30 | exports.code = translate.code; 31 | 32 | exports.codeFromName = translate.codeFromName; 33 | 34 | exports.codeOnlyFromName = function (OID, key) { 35 | var f = translate.codeFromName(OID); 36 | return function (input) { 37 | if (input && input[key]) { 38 | return f(input[key]).code; 39 | } else { 40 | return null; 41 | } 42 | }; 43 | }; 44 | 45 | exports.time = translate.time; 46 | 47 | exports.use = function (key) { 48 | return function (input) { 49 | var value = input && input[key]; 50 | if (value) { 51 | return translate.acronymize(value); 52 | } else { 53 | return null; 54 | } 55 | }; 56 | }; 57 | 58 | exports.typeCD = { 59 | "xsi:type": "CD" 60 | }; 61 | 62 | exports.typeCE = { 63 | "xsi:type": "CE" 64 | }; 65 | 66 | exports.nextReference = function (referenceKey) { 67 | return function (input, context) { 68 | return context.nextReference(referenceKey); 69 | }; 70 | }; 71 | 72 | exports.sameReference = function (referenceKey) { 73 | return function (input, context) { 74 | return context.sameReference(referenceKey); 75 | }; 76 | }; 77 | 78 | exports.deepInputProperty = function (deepProperty, defaultValue) { 79 | return function (input) { 80 | var value = input[deepProperty]; 81 | value = bbuo.exists(value) ? value : defaultValue; 82 | if (typeof value !== 'string') { 83 | value = value.toString(); 84 | } 85 | return value; 86 | }; 87 | }; 88 | 89 | exports.deepInputDate = function (deepProperty, defaultValue) { 90 | return function (input) { 91 | var value = input[deepProperty]; 92 | if (!bbuo.exists(value)) { 93 | return defaultValue; 94 | } else { 95 | value = bbud.modelToDate({ 96 | date: value.date, 97 | precision: value.precision // workaround a bug in bbud. Changes precision. 98 | }); 99 | if (bbuo.exists(value)) { 100 | return value; 101 | } else { 102 | return defaultValue; 103 | } 104 | } 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /test/util/jsonutil.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | var orderByTemplateId = function (input) { 7 | var elementsPerTID = input.reduce(function (r, element) { 8 | var subElement = element['observation'] || element['supply'] || element['act']; 9 | if (subElement) { 10 | var templateNode = subElement[0] && subElement[0].templateId; 11 | if (templateNode) { 12 | var templateId = templateNode[0] && templateNode[0]['$'] && templateNode[0]['$'].root; 13 | if (templateId) { 14 | if (!r[templateId]) { 15 | r[templateId] = []; 16 | } 17 | r[templateId].push(element); 18 | return r; 19 | } 20 | } 21 | } 22 | if (!r.unknown) { 23 | r.unknown = []; 24 | } 25 | r.unknown.push(element); 26 | return r; 27 | }, {}); 28 | var templateIds = Object.keys(elementsPerTID); 29 | templateIds.sort(); 30 | var result = templateIds.reduce(function (r, templateId) { 31 | r = r.concat(elementsPerTID[templateId]); 32 | return r; 33 | }, []); 34 | return result; 35 | }; 36 | 37 | var orderByKeys = exports.orderByKeys = function orderByKeys(input, parentKey) { 38 | if (Array.isArray(input)) { 39 | if (parentKey === 'entryRelationship') { 40 | input = orderByTemplateId(input); 41 | } 42 | var aresult = input.map(function (element) { 43 | if (element && (typeof element === 'object')) { 44 | return orderByKeys(element); 45 | } else { 46 | return element; 47 | } 48 | }); 49 | return aresult; 50 | } else { 51 | var result = {}; 52 | var keys = Object.keys(input); 53 | keys.sort(); 54 | keys.forEach(function (key) { 55 | if (input[key] && (typeof input[key] === 'object')) { 56 | result[key] = orderByKeys(input[key], key); 57 | } else { 58 | result[key] = input[key]; 59 | } 60 | }); 61 | return result; 62 | } 63 | }; 64 | 65 | exports.getDeepValue = function (root, path) { 66 | var keys = path.split('.'); 67 | var node = root; 68 | keys.forEach(function (key) { 69 | if (node.hasOwnProperty(key)) { 70 | node = node[key]; 71 | } else { 72 | node = null; 73 | } 74 | if ((node === undefined) || (node === null)) { 75 | return null; 76 | } 77 | }); 78 | return node; 79 | }; 80 | 81 | exports.fileToJSON = function (directory, filename) { 82 | var p = path.join(directory, filename); 83 | var content = fs.readFileSync(p); 84 | var result = JSON.parse(content); 85 | return result; 86 | }; 87 | 88 | exports.JSONToFile = function (json, directory, filename) { 89 | var p = path.join(directory, filename); 90 | var content = JSON.stringify(json, null, 2); 91 | fs.writeFileSync(p, content); 92 | }; 93 | -------------------------------------------------------------------------------- /test/xmlmods/bbParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | module.exports = [{ 6 | xpath: "//*[@nullFlavor]", 7 | action: "removeNode" 8 | }, { 9 | xpath: "//h:text", 10 | action: "removeNode" 11 | }, { 12 | xpath: " //h:telecom[@use='WP']", 13 | action: "normalizeTelNumber" 14 | }, { 15 | xpath: "//h:originalText", 16 | action: "removeNode" 17 | }, { 18 | xpath: "//h:code[@codeSystemVersion]", 19 | action: "removeAttribute", 20 | params: "codeSystemVersion" 21 | }, { 22 | xpath: "//*[@assigningAuthorityName]", 23 | action: "removeAttribute", 24 | params: "assigningAuthorityName" 25 | }, { 26 | xpath: t.medSupplyOrder + '/h:product', 27 | action: "removeNode" 28 | }, { 29 | xpath: t.medSupplyOrder + '/h:performer', 30 | action: "removeNode" 31 | }, { 32 | xpath: t.medDispense + '/h:product', 33 | action: "removeNode", 34 | comment: "not read by parser" 35 | }, { 36 | xpath: t.medDispense + '/h:product', 37 | action: "removeNode", 38 | comment: "not read by parser" 39 | }, { 40 | xpath: t.medDispense + '/h:quantity', 41 | action: "removeNode", 42 | comment: "not read by parser" 43 | }, { 44 | xpath: t.medDispense + '/h:repeatNumber', 45 | action: "removeNode", 46 | comment: "not read by parser" 47 | }, { 48 | xpath: t.medDispense + '/h:effectiveTime', 49 | action: "removeNode", 50 | comment: "not read by parser" 51 | }, { 52 | xpath: t.medDispense + '/h:performer/h:assignedEntity/h:assignedPerson', 53 | action: "removeNode", 54 | comment: "not read by parser" 55 | }, { 56 | xpath: t.medActivityInstructions, 57 | action: "removeNode", 58 | comment: "not read by parser" 59 | }, { 60 | xpath: t.immRefusalReason + '/h:id', 61 | action: "removeNode", 62 | comment: "not read by parser" 63 | }, { 64 | xpath: t.procProductInstance, 65 | action: "removeNode", 66 | comment: "not read by parser" 67 | }, { 68 | xpath: t.payersSection + '/.//h:time', 69 | action: "removeNode" 70 | }, { 71 | xpath: t.probObservation + '/h:code', 72 | action: "removeNode" 73 | }, { 74 | xpath: t.vitalsSection + '/..', 75 | action: "flatten", 76 | params: "2.16.840.1.113883.10.20.22.4.27" 77 | }, { 78 | xpath: t.vitalsSection + '/h:entry', 79 | action: 'removeNode' 80 | }, { 81 | xpath: '//h:recordTarget/h:patientRole/h:providerOrganization', 82 | action: 'removeNode' 83 | }, { 84 | xpath: t.procActEither + "/h:statusCode[@code=\"completed\"]", 85 | action: "addAttribute", 86 | params: { 87 | "codeSystem": "2.16.840.1.113883.11.20.9.22", 88 | "codeSystemName": "ActStatus", 89 | "displayName": "Completed" 90 | }, 91 | comment: "generator fills full code information" 92 | }, { 93 | xpath: t.procActEither + "/h:statusCode[@code=\"aborted\"]", 94 | action: "addAttribute", 95 | params: { 96 | "codeSystem": "2.16.840.1.113883.11.20.9.22", 97 | "codeSystemName": "ActStatus", 98 | "displayName": "Aborted" 99 | }, 100 | comment: "generator fills full code information" 101 | }, { 102 | xpath: "//h:effectiveTime[@value] | //h:effectiveTime/h:low[@value] | //h:effectiveTime/h:high[@value]", 103 | action: "removeTimezone", 104 | comment: "parser bug: timezones are not read" 105 | }]; 106 | -------------------------------------------------------------------------------- /test/util/xml2jsutil.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var xml2js = require('xml2js'); 4 | var jsonutil = require('../util/jsonutil'); 5 | 6 | var findSection = exports.findSection = (function () { 7 | var templateIdsForSection = { 8 | 'allergies': ["2.16.840.1.113883.10.20.22.2.6", "2.16.840.1.113883.10.20.22.2.6.1"], 9 | 'medications': ["2.16.840.1.113883.10.20.22.2.1", "2.16.840.1.113883.10.20.22.2.1.1"], 10 | 'immunizations': ["2.16.840.1.113883.10.20.22.2.2", "2.16.840.1.113883.10.20.22.2.2.1"], 11 | 'procedures': ["2.16.840.1.113883.10.20.22.2.7", "2.16.840.1.113883.10.20.22.2.7.1"], 12 | 'encounters': ["2.16.840.1.113883.10.20.22.2.22"], 13 | 'payers': ["2.16.840.1.113883.10.20.22.2.18"], 14 | 'plan_of_care': ["2.16.840.1.113883.10.20.22.2.10"], 15 | 'problems': ["2.16.840.1.113883.10.20.22.2.5", "2.16.840.1.113883.10.20.22.2.5.1"], 16 | 'social_history': ["2.16.840.1.113883.10.20.22.2.17"], 17 | 'vitals': ["2.16.840.1.113883.10.20.22.2.4", "2.16.840.1.113883.10.20.22.2.4.1"], 18 | 'results': ["2.16.840.1.113883.10.20.22.2.3", "2.16.840.1.113883.10.20.22.2.3.1"] 19 | }; 20 | 21 | var findNormalSection = function (ccd, sectionName) { 22 | var root = jsonutil.getDeepValue(ccd, 'ClinicalDocument.component.0.structuredBody.0.component'); 23 | if (root) { 24 | var n = root.length; 25 | var templateIds = templateIdsForSection[sectionName]; 26 | for (var i = 0; i < n; ++i) { 27 | var sectionInfo = root[i].section[0]; 28 | var ids = sectionInfo.templateId; 29 | if (ids) { 30 | for (var j = 0; j < ids.length; ++j) { 31 | var id = ids[j]; 32 | for (var k = 0; k < templateIds.length; ++k) { 33 | var templateId = templateIds[k]; 34 | if (id['$'].root === templateId) { 35 | return root[i].section[0]; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | return null; 43 | }; 44 | 45 | var findDemographics = function (ccd) { 46 | var result = jsonutil.getDeepValue(ccd, 'ClinicalDocument.recordTarget.0.patientRole.0'); 47 | return result; 48 | }; 49 | 50 | return function (ccd, sectionName) { 51 | if (sectionName === 'demographics') { 52 | return findDemographics(ccd); 53 | } else { 54 | return findNormalSection(ccd, sectionName); 55 | } 56 | }; 57 | })(); 58 | 59 | var toOrderedJSON = exports.toOrderedJSON = function (xml, callback) { 60 | var parser = new xml2js.Parser({ 61 | async: false, 62 | normalize: true 63 | }); 64 | parser.parseString(xml, function (err, result) { 65 | if (err) { 66 | callback(err); 67 | } else { 68 | var orderedResult = jsonutil.orderByKeys(result); 69 | callback(err, orderedResult); 70 | } 71 | }); 72 | }; 73 | 74 | var sectionNames = ['demographics', 'allergies', 'medications', 'immunizations', 'procedures', 'encounters', 'payers', 'plan_of_care', 'problems', 'social_history', 'vitals', 'results']; 75 | 76 | exports.toOrderedSectionJSONs = function (xml, callback) { 77 | toOrderedJSON(xml, function (err, result) { 78 | if (err) { 79 | callback(err); 80 | } else { 81 | var resultSectionized = sectionNames.reduce(function (r, name) { 82 | var sectionJSON = findSection(result, name); 83 | r[name] = sectionJSON; 84 | return r; 85 | }, {}); 86 | callback(null, resultSectionized); 87 | } 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /lib/entryLevel/resultEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | 7 | var contentModifier = require("../contentModifier"); 8 | 9 | var required = contentModifier.required; 10 | 11 | var resultObservation = { 12 | key: "observation", 13 | attributes: { 14 | classCode: "OBS", 15 | moodCode: "EVN" 16 | }, 17 | content: [ 18 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.2"), 19 | fieldLevel.id, { 20 | key: "code", 21 | attributes: leafLevel.code, 22 | dataKey: "result", 23 | required: true 24 | }, 25 | fieldLevel.text(leafLevel.nextReference("result")), 26 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 27 | key: "value", 28 | attributes: { 29 | "xsi:type": function (input) { 30 | return input.text ? "ST" : "PQ"; 31 | }, 32 | value: leafLevel.inputProperty("value"), 33 | unit: leafLevel.inputProperty("unit") 34 | }, 35 | text: leafLevel.inputProperty("text"), 36 | existsWhen: condition.eitherKeyExists("value", "text"), 37 | required: true 38 | }, { 39 | key: "interpretationCode", 40 | attributes: { 41 | code: function (input) { 42 | return input.code; 43 | }, 44 | codeSystem: "2.16.840.1.113883.5.83", 45 | displayName: function (input) { 46 | return input.name || input; 47 | }, 48 | codeSystemName: "ObservationInterpretation" 49 | }, 50 | dataKey: "interpretations" 51 | }, { 52 | key: "referenceRange", 53 | content: { 54 | key: "observationRange", 55 | content: [{ 56 | key: "text", 57 | text: leafLevel.input, 58 | dataKey: "range" 59 | }, { 60 | key: "value", 61 | attributes: { 62 | "xsi:type": "IVL_PQ" 63 | }, 64 | content: [{ 65 | key: "low", 66 | attributes: { 67 | value: leafLevel.inputProperty("low"), 68 | unit: leafLevel.inputProperty("unit") 69 | }, 70 | existsWhen: condition.keyExists("low") 71 | }, { 72 | key: "high", 73 | attributes: { 74 | value: leafLevel.inputProperty("high"), 75 | unit: leafLevel.inputProperty("unit") 76 | }, 77 | existsWhen: condition.keyExists("high") 78 | }], 79 | existsWhen: condition.eitherKeyExists("low", "high") 80 | }], 81 | required: true 82 | }, 83 | dataKey: "reference_range" 84 | } 85 | ], 86 | notIplemented: [ 87 | "variable statusCode", 88 | "methodCode", 89 | "targetSiteCode", 90 | "author" 91 | ] 92 | }; 93 | 94 | exports.resultOrganizer = { 95 | key: "organizer", 96 | attributes: { 97 | classCode: "BATTERY", 98 | moodCode: "EVN" 99 | }, 100 | content: [ 101 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.1"), 102 | fieldLevel.uniqueId, 103 | fieldLevel.id, { 104 | key: "code", 105 | attributes: leafLevel.code, 106 | content: { 107 | key: "translation", 108 | attributes: leafLevel.code, 109 | dataKey: "translations" 110 | }, 111 | dataKey: "result_set", 112 | required: true 113 | }, 114 | fieldLevel.statusCodeCompleted, { 115 | key: "component", 116 | content: [ 117 | [resultObservation, required] 118 | ], 119 | dataKey: "results", 120 | required: true 121 | } 122 | ], 123 | notIplemented: [ 124 | "variable @classCode", 125 | "variable statusCode" 126 | ] 127 | }; 128 | -------------------------------------------------------------------------------- /docs/old_generator.md: -------------------------------------------------------------------------------- 1 | ## CCDA Generation: Introduction 2 | This module converts data in JSON format (originally parsed from a CCDA) back to CCDA/blue-button format. It determines the section template (e.g. allergies, immunizations, etc) to which the JSON data belongs, runs the data through the appropriate templater, and generates the corresponding CCDA/blue-button section. An entire CCDA document can be generated by iteratively running the JSON data through the templaters for each section. 3 | 4 | The API exposes genWholeCCDA() for this purpose, which takes in CCDA data in JSON format as a parameter and converts it into CCDA/XML. 5 | 6 | The module uses libxmljs for its templaters and uses a JS XML DOM implementation (https://github.com/jindw/xmldom) to traverse the generated and expected XML documents for testing and compare them by node (tagName) and by attribute and value. 7 | 8 | ## CCDA Generation: Testing 9 | A suite of tests and a test class (test/test-generator and test/test-lib, respectively) help in verifying that the generated CCDA is accurate. Tests include: 10 | - Parsing CCDA to JSON, regenerating the CCDA from the new JSON file, and comparing the original and generated CCDA files for differences. 11 | - Parsing, generating, and parsing again, comparing the first parsed JSON data with the second parsed JSON data for equality. 12 | - Comparing a specific, single section of CCDA to the original specific, single section, to achieve testing granularity. 13 | - Testing the generator against the entire corpora of CCDA documents at: https://github.com/chb/sample_ccdas using the internal ccda_explorer module 14 | 15 | The testing class/library provides methods to compare two XML/CCDA documents by recursively walking the XML document tree and comparing the documents node-by-node. Assertion-based or diff-based testing can be used with this library by setting the appropriate flags. The default settings ignore comments, whitespace, newlines, tab or text nodes. Here is an example of diff-based testing output after testing the CCDA Procedures Section: 16 | 17 | ```` 18 | PROCESSING PROCEDURES 19 | Error: Generated number of child nodes different from expected (generated: 0 at lineNumber: 8, expected: 1 at lineNumber:12 20 | Error: Generated number of child nodes different from expected (generated: 11 at lineNumber: 10, expected: 10 at lineNumber:32 21 | Attributes mismatch. Different lengths: 1 attributes but expected 0 attributes @ lineNumber: 70, 94 22 | Error: Generated number of child nodes different from expected (generated: 10 at lineNumber: 71, expected: 11 at lineNumber:95 23 | Attributes mismatch. Different lengths: 1 attributes but expected 0 attributes @ lineNumber: 119, 149 24 | 25 | Error: Attributes mismatch: Encountered: moodCode="EVN" but expected: moodCode="INT" @ lineNumber: 120, 150 26 | Error: Encountered different tagNames: Encountered but expected , lineNumber: 130, 161 27 | Error: Generated number of child nodes different from expected (generated: 4 at lineNumber: 151, expected: 5 at lineNumber:182 28 | ERRORS: 8 29 | ```` 30 | 31 | 32 | 33 | **Alterable flags (in test-lib.js)**: 34 | -PROMPT_TO_SKIP: If set to true, will prompt the user to either skip or not skip the failed test. 35 | -DIFF (default): If set to true, will continue execution even upon failing a test and will output all of the errors/differences to the console. This is the default setting. 36 | 37 | **Alterable settings (testXML.error_settings in test-lib.js)**: 38 | -"silence_cap": If set to true, will silence the output of capitalization errors. False by default. 39 | -"silence_len": If set to true, will silence the output of attribute length errors (i.e. actual node has 2 attributes but expected node has 3 attributes). False by default. 40 | 41 | **Test suite settings**: 42 | -TEST_CCDA_SAMPLES: uses ccda-explorer to test against sample_ccdas 43 | -TEST_CCD: tests against one generic sample ccda 44 | -TEST_SECTIONS: tests each section individually 45 | 46 | 47 | *** 48 | 49 | -------------------------------------------------------------------------------- /lib/translate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var moment = require("moment"); 4 | var bbm = require("@amida-tech/blue-button-meta"); 5 | 6 | var css = bbm.code_systems; 7 | 8 | exports.codeFromName = function (OID) { 9 | return function (input) { 10 | var cs = css.find(OID); 11 | var code = cs ? cs.displayNameCode(input) : undefined; 12 | var systemInfo = cs.systemId(OID); 13 | return { 14 | "displayName": cs.codeDisplayName(code) || input.name || input, 15 | "code": code, 16 | "codeSystem": systemInfo.codeSystem, 17 | "codeSystemName": systemInfo.codeSystemName 18 | }; 19 | }; 20 | }; 21 | 22 | exports.code = function (input) { 23 | var result = {}; 24 | if (input.code) { 25 | result.code = input.code; 26 | } 27 | 28 | if (input.name) { 29 | result.displayName = input.name; 30 | } 31 | 32 | var code_system = input.code_system || (input.code_system_name && css.findFromName(input.code_system_name)); 33 | if (code_system) { 34 | result.codeSystem = code_system; 35 | } 36 | 37 | if (input.code_system_name) { 38 | result.codeSystemName = input.code_system_name; 39 | } 40 | 41 | return result; 42 | }; 43 | 44 | var precisionToFormat = { 45 | year: 'YYYY', 46 | month: 'YYYYMM', 47 | day: 'YYYYMMDD', 48 | hour: 'YYYYMMDDHH', 49 | minute: 'YYYYMMDDHHMM', 50 | second: 'YYYYMMDDHHmmssZZ', 51 | subsecond: 'YYYYMMDDHHmmss.SSSZZ' 52 | }; 53 | 54 | exports.time = function (input) { 55 | var m = moment.parseZone(input.date); 56 | var formatSpec = precisionToFormat[input.precision]; 57 | var result = m.format(formatSpec); 58 | return result; 59 | }; 60 | 61 | var acronymize = exports.acronymize = function (string) { 62 | var ret = string.split(" "); 63 | var fL = ret[0].slice(0, 1); 64 | var lL = ret[1].slice(0, 1); 65 | fL = fL.toUpperCase(); 66 | lL = lL.toUpperCase(); 67 | ret = fL + lL; 68 | if (ret === "PH") { 69 | ret = "HP"; 70 | } 71 | if (ret === "HA") { 72 | ret = "H"; 73 | } 74 | return ret; 75 | }; 76 | 77 | exports.telecom = function (input) { 78 | var transformPhones = function (input) { 79 | var phones = input.phone; 80 | if (phones) { 81 | return phones.reduce(function (r, phone) { 82 | if (phone && phone.number) { 83 | var attrs = { 84 | value: "tel:" + phone.number 85 | }; 86 | if (phone.type) { 87 | attrs.use = acronymize(phone.type); 88 | } 89 | r.push(attrs); 90 | } 91 | return r; 92 | }, []); 93 | } else { 94 | return []; 95 | } 96 | }; 97 | 98 | var transformEmails = function (input) { 99 | var emails = input.email; 100 | if (emails) { 101 | return emails.reduce(function (r, email) { 102 | if (email && email.address) { 103 | var attrs = { 104 | value: "mailto:" + email.address 105 | }; 106 | if (email.type) { 107 | attrs.use = acronymize(email.type); 108 | } 109 | r.push(attrs); 110 | } 111 | return r; 112 | }, []); 113 | } else { 114 | return []; 115 | } 116 | }; 117 | 118 | var result = [].concat(transformPhones(input), transformEmails(input)); 119 | return result.length === 0 ? null : result; 120 | }; 121 | 122 | var nameSingle = function (input) { 123 | var given = null; 124 | if (input.first) { 125 | given = [input.first]; 126 | if (input.middle && input.middle[0]) { 127 | given.push(input.middle[0]); 128 | } 129 | } 130 | return { 131 | prefix: input.prefix, 132 | given: given, 133 | family: input.last, 134 | suffix: input.suffix 135 | }; 136 | }; 137 | 138 | exports.name = function (input) { 139 | if (Array.isArray(input)) { 140 | return input.map(function (e) { 141 | return nameSingle(e); 142 | }); 143 | } else { 144 | return nameSingle(input); 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | 3 | module.exports = function (grunt) { 4 | var bbg = require('./index'); 5 | var path = require('path'); 6 | 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | grunt.loadNpmTasks('grunt-contrib-watch'); 9 | grunt.loadNpmTasks('grunt-contrib-connect'); 10 | grunt.loadNpmTasks('grunt-coveralls'); 11 | grunt.loadNpmTasks('grunt-jsbeautifier'); 12 | grunt.loadNpmTasks('grunt-run'); 13 | 14 | // Project configuration. 15 | grunt.initConfig({ 16 | pkg: grunt.file.readJSON('package.json'), 17 | jshint: { 18 | files: ['*.js', '*.json', './lib/*.js', './lib/**/*.js', './test/**/*.js'], 19 | options: { 20 | browser: true, 21 | smarttabs: true, 22 | curly: true, 23 | eqeqeq: true, 24 | immed: true, 25 | latedef: true, 26 | newcap: true, 27 | noarg: true, 28 | sub: true, 29 | undef: false, 30 | boss: true, 31 | eqnull: true, 32 | node: true, 33 | expr: true, 34 | globals: { 35 | 'it': true, 36 | 'xit': true, 37 | 'xdescribe': true, 38 | 'describe': true, 39 | 'expect': true, 40 | 'before': true, 41 | 'beforeAll': true, 42 | 'after': true, 43 | 'afterAll': true, 44 | 'done': true 45 | } 46 | } 47 | }, 48 | watch: { 49 | all: { 50 | files: ['./lib/*.js', './lib/**/*.js', '*.js', './test/**/*.js'], 51 | tasks: ['default'] 52 | } 53 | }, 54 | jsbeautifier: { 55 | beautify: { 56 | src: ['Gruntfile.js', 'lib/*.js', 'lib/**/*.js', 'test/**/*.js', '*.js', 'test/xmlmods/*.json'], 57 | options: { 58 | config: '.jsbeautifyrc' 59 | } 60 | }, 61 | check: { 62 | src: ['Gruntfile.js', 'lib/*.js', 'lib/**/*.js', 'test/**/*.js', '*.js', 'test/xmlmods/*.json'], 63 | options: { 64 | mode: 'VERIFY_ONLY', 65 | config: '.jsbeautifyrc' 66 | } 67 | } 68 | }, 69 | run: { 70 | test: { 71 | exec: 'npx jest' 72 | }, 73 | coverage: { 74 | exec: 'npx jest --coverage' 75 | } 76 | }, 77 | connect: { 78 | server: { 79 | options: { 80 | port: 8000, 81 | hostname: '127.0.0.1' 82 | } 83 | } 84 | } 85 | }); 86 | 87 | grunt.registerTask('mkdir-test-temp', 'create test temporary directories', function () { 88 | grunt.file.mkdir('test/fixtures/files/generated'); 89 | }); 90 | grunt.registerTask('json-to-xml-main', 'converts json files to xml', function (src, dest) { 91 | grunt.file.recurse(src, function (abspath, rootdir, subdir, filename) { 92 | var content = grunt.file.read(abspath); 93 | var json = JSON.parse(content); 94 | var xml = bbg.generateCCD(json); 95 | var xmlFilename = path.basename(filename, path.extname(filename)) + '.xml'; 96 | 97 | var destPath = subdir ? path.join(dest, subdir, xmlFilename) : path.join(dest, xmlFilename); 98 | grunt.file.write(destPath, xml); 99 | }); 100 | }); 101 | 102 | //JS beautifier 103 | grunt.registerTask('beautify', ['jsbeautifier:beautify']); 104 | 105 | // generates xml files from source jsons. 106 | grunt.registerTask('json-to-xml', ['mkdir-test-temp', 'json-to-xml-main:test/fixtures/json:test/fixtures/files/generated/json_to_xml']); 107 | // generates xml files from generated jsons. 108 | grunt.registerTask('re-json-to-xml', ['mkdir-test-temp', 'json-to-xml-main:test/fixtures/files/generated/xml_to_json:test/fixtures/files/generated/re_json_to_xml']); 109 | 110 | // Default task. 111 | grunt.registerTask('default', ['beautify', 'jshint', 'mkdir-test-temp', 'test']); 112 | grunt.registerTask('test', ['run:test']); 113 | grunt.registerTask('coverage', ['run:coverage']); 114 | 115 | grunt.registerTask('commit', ['jshint', 'mkdir-test-temp', 'test']); 116 | grunt.registerTask('timestamp', function () { 117 | grunt.log.subhead(Date()); 118 | }); 119 | }; 120 | -------------------------------------------------------------------------------- /test/sample_runs/test-xml-vs-generatedxml.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var path = require('path'); 5 | var bb = require('@amida-tech/blue-button'); 6 | var bbg = require('../../index'); 7 | 8 | var jsonutil = require('../util/jsonutil'); 9 | var util = require('../util'); 10 | 11 | var bbParserMods = require('../xmlmods/bbParser'); 12 | var bbGeneratorMods = require('../xmlmods/bbGenerator'); 13 | var ccd1ParserMods = require('../xmlmods/ccd1Parser'); 14 | var ccd1GeneratorMods = require('../xmlmods/ccd1Generator'); 15 | var viteraParserMods = require('../xmlmods/viteraParser'); 16 | var viteraGeneratorMods = require('../xmlmods/viteraGenerator'); 17 | 18 | describe('xml vs parse generate xml ', function () { 19 | var generatedDir = null; 20 | var sampleDir = null; 21 | 22 | beforeAll(function () { 23 | generatedDir = path.join(__dirname, "../fixtures/files/generated"); 24 | sampleDir = path.join(__dirname, "../fixtures/files/ccda_xml"); 25 | expect(generatedDir).toBeDefined(); 26 | expect(sampleDir).toBeDefined(); 27 | }); 28 | 29 | var testSampleFile = function (filename, validate, addlParserMods, addlGeneratorMods, limited) { 30 | return function () { 31 | var xmlRaw; 32 | var xmlObj; 33 | var xmlGeneratedObj; 34 | 35 | it('read xml', function () { 36 | xmlRaw = fs.readFileSync(path.join(sampleDir, filename + '.xml')).toString(); 37 | expect(xmlRaw).toBeDefined(); 38 | 39 | }); 40 | 41 | it('xml2js original', function (done) { 42 | var mods = bbParserMods; 43 | if (addlParserMods) { 44 | mods = mods.concat(addlParserMods); 45 | } 46 | util.toSectionJSONs(xmlRaw, mods, function (err, result) { 47 | xmlObj = result; 48 | done(err); 49 | }); 50 | }); 51 | 52 | it('xml2js generated', function (done) { 53 | var mods = bbGeneratorMods; 54 | if (addlGeneratorMods) { 55 | mods = mods.concat(addlGeneratorMods); 56 | } 57 | util.toBBSectionJSONs(xmlRaw, validate, mods, function (err, result) { 58 | xmlGeneratedObj = result; 59 | done(err); 60 | }); 61 | }); 62 | 63 | var compareSection = function (section, sectionGenerated, baseName) { 64 | jsonutil.JSONToFile(section, generatedDir, "o_" + baseName + ".json"); 65 | jsonutil.JSONToFile(sectionGenerated, generatedDir, "g_" + baseName + ".json"); 66 | 67 | expect(sectionGenerated).toEqual(section); 68 | }; 69 | 70 | var findCompareSection = function (sectionName) { 71 | var section = xmlObj[sectionName]; 72 | var sectionGenerated = xmlGeneratedObj[sectionName]; 73 | 74 | compareSection(section, sectionGenerated, filename + '_' + sectionName); 75 | }; 76 | 77 | it('allergies', function () { 78 | findCompareSection('allergies'); 79 | }); 80 | 81 | it('medications', function () { 82 | findCompareSection('medications'); 83 | }); 84 | 85 | it('immunizations', function () { 86 | findCompareSection('immunizations'); 87 | }); 88 | 89 | it('procedures', function () { 90 | findCompareSection('procedures'); 91 | }); 92 | 93 | it('encounters', function () { 94 | findCompareSection('encounters'); 95 | }); 96 | 97 | it('payers', function () { 98 | findCompareSection('payers'); 99 | }); 100 | 101 | it('plan_of_care', function () { 102 | findCompareSection('plan_of_care'); 103 | }); 104 | 105 | it('problems', function () { 106 | findCompareSection('problems'); 107 | }); 108 | 109 | if (!limited) { 110 | it('social_history', function () { 111 | findCompareSection('social_history'); 112 | }); 113 | } 114 | 115 | it('vitals', function () { 116 | findCompareSection('vitals'); 117 | }); 118 | 119 | it('results', function () { 120 | findCompareSection('results'); 121 | }); 122 | 123 | it('demographics', function () { 124 | findCompareSection('demographics'); 125 | }); 126 | 127 | }; 128 | }; 129 | 130 | describe('CCD_1.xml', testSampleFile('CCD_1', true, ccd1ParserMods, ccd1GeneratorMods)); 131 | 132 | //describe('Vitera.xml', testSampleFile('Vitera', false, viteraParserMods, viteraGeneratorMods, true)); 133 | }); 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Blue Button Generate 2 | ==================== 3 | 4 | Blue Button CCDA Generator 5 | 6 | [![NPM](https://nodei.co/npm/@amida-tech/blue-button-generate.png)](https://nodei.co/npm/@amida-tech/blue-button-generate/) 7 | 8 | blue-button-generate is a module to generate CCDA files from JSON data. Currently it only supports [blue-button](https://github.com/amida-tech/blue-button) JSON data model. 9 | 10 | ## Usage 11 | 12 | ``` javascript 13 | var fs = require('fs'); 14 | var bb = require('@amida-tech/blue-button'); 15 | var bbg = require('@amida-tech/blue-button-generate'); 16 | 17 | var xmlString = fs.readFileSync('test/fixtures/files/ccda_xml/CCD_1.xml', 'utf-8'); 18 | var record = bb.parseString(xmlString); 19 | 20 | // ... 21 | // changes to record 22 | // ... 23 | 24 | // get back xml as text 25 | var updatedXmlString = bbg.generateCCD(record); 26 | 27 | ``` 28 | 29 | ## Implementation 30 | 31 | blue-button-generate uses javascript template objects for implementation. Each template in CCDA is represented with an object. As an example Reaction Observation object is shown 32 | ``` javascript 33 | var reactionObservation = exports.reactionObservation = { 34 | key: "observation", 35 | attributes: { 36 | "classCode": "OBS", 37 | "moodCode": "EVN" 38 | }, 39 | content: [ 40 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.9"), 41 | fieldLevel.id, 42 | fieldLevel.nullFlavor("code"), 43 | fieldLevel.text(leafLevel.sameReference("reaction")), 44 | fieldLevel.statusCodeCompleted, 45 | fieldLevel.effectiveTime, { 46 | key: "value", 47 | attributes: [ 48 | leafLevel.typeCD, 49 | leafLevel.code 50 | ], 51 | dataKey: 'reaction', 52 | existsWhen: condition.codeOrDisplayname, 53 | required: true 54 | }, { 55 | key: "entryRelationship", 56 | attributes: { 57 | "typeCode": "SUBJ", 58 | "inversionInd": "true" 59 | }, 60 | content: severityObservation, 61 | existsWhen: condition.keyExists('severity') 62 | } 63 | ] 64 | }; 65 | ``` 66 | 67 | This template is internally used with a call 68 | ``` javascript 69 | js2xml.update(xmlDoc, input, context, reactionObservation); 70 | ``` 71 | where `xmlDoc` is the parent xml document (Allergy Intolerance Observation) and `input` is the immediate parent of [bluebutton.js](https://github.com/blue-button/bluebutton.js) object that describes Reaction Observation. `context` is internally used for indices in text references. 72 | 73 | ### Motivation 74 | 75 | This approach is an alternative to direct programming or text based templates such as in [bluebutton.js](https://github.com/blue-button/bluebutton.js) and is motivated by the following 76 | * Each template directly follows the actual specification. One can easily match each node in the template to the actual statements in the specification. 77 | * Individual templates can be tested independently without any additional flags or programming. 78 | * Required elements are specified in the template and get `nullFlavor` automatically when no data exists. 79 | * No coding required to add new templates. 80 | * It is also a step in the right direction for the possible future directions 81 | * Factoring out data model dependencies so that blue-button](https://github.com/amida-tech/blue-button) data model changes or other data models can be accomodated more easily 82 | * Automatic generation of templates from [blue-button](https://github.com/amida-tech/blue-button) like CCDA parsers. 83 | 84 | ### Template Structure 85 | 86 | The following are the properties of the templates 87 | * `key`: This is the name for the xml element. 88 | * `attributes`: This describes the attributes of the element. `attributes` can be an object of with `key` and `value` pairs for each attribute or it can be an array of such objects. Each attribute object or can be a function with `input` argument that returns attributes. 89 | * `text`: This is a function with `input` attribute that returns text value of the element. 90 | * `content`: This is an array of other templates that describe the children of the element. For a single child an object can be used. 91 | * `dataKey`: This is the property of `input` that serves as the data for the template. 92 | * `required`: This identifies if template is required or not. If template is required and there is not value in the `input` a `nullFlavor` node is created. 93 | * `dataTransform`: This is a function to transform the input. 94 | * `existWhen`: This is a boolean function with `input` argument to describe it the elements should exists or not. 95 | -------------------------------------------------------------------------------- /test/xmlmods/viteraGenerator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | module.exports = [{ 6 | xpath: t.allergiesSection + '/.//h:effectiveTime[not(@value | h:low | h:high)]', 7 | action: "removeNode" 8 | }, { 9 | xpath: t.allergiesSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.6"]', 10 | action: "removeNode", 11 | comment: "this templateId does not exist in the file" 12 | }, { 13 | xpath: t.medSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.1"]', 14 | action: "removeNode", 15 | comment: "this templateId does not exist in the file" 16 | }, { 17 | xpath: t.medDispense + '/h:performer', 18 | action: "addAttribute", 19 | params: { 20 | "typeCode": "PRF" 21 | }, 22 | comment: "needs more research" 23 | }, { 24 | xpath: t.medDispense + '/h:performer/h:assignedEntity', 25 | action: "addAttribute", 26 | params: { 27 | "classCode": "ASSIGNED" 28 | }, 29 | comment: "needs more research" 30 | }, { 31 | xpath: t.immSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.2"]', 32 | action: "removeNode", 33 | comment: "this templateId does not exist in the file" 34 | }, { 35 | xpath: t.immActivity + '/h:performer', 36 | action: "addAttribute", 37 | params: { 38 | "typeCode": "PRF" 39 | }, 40 | comment: "needs more research" 41 | }, { 42 | xpath: t.immActivity + '/h:performer/h:assignedEntity', 43 | action: "addAttribute", 44 | params: { 45 | "classCode": "ASSIGNED" 46 | }, 47 | comment: "needs more research" 48 | }, { 49 | xpath: t.procSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.7"]', 50 | action: "removeNode", 51 | comment: "this templateId does not exist in the file" 52 | }, { 53 | xpath: t.procActProc + '/h:participant/h:participantRole/h:templateId', 54 | action: "removeNode", 55 | comment: "error in file: this should be in participantRole" 56 | }, { 57 | xpath: t.procActProc + '/h:performer', 58 | action: "addAttribute", 59 | params: { 60 | "typeCode": "PRF" 61 | }, 62 | comment: "needs more research" 63 | }, { 64 | xpath: t.procActProc + '/h:performer/h:assignedEntity', 65 | action: "addAttribute", 66 | params: { 67 | "classCode": "ASSIGNED" 68 | }, 69 | comment: "needs more research" 70 | }, { 71 | xpath: t.encSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.22.1"]', 72 | action: "removeNode", 73 | comment: "this templateId does not exist in the file" 74 | }, { 75 | xpath: t.encAct + '/h:participant/h:participantRole/h:templateId', 76 | action: "removeNode", 77 | comment: "error in file: this should be in participantRole" 78 | }, { 79 | xpath: t.encAct + '/h:performer', 80 | action: "addAttribute", 81 | params: { 82 | "typeCode": "PRF" 83 | }, 84 | comment: "needs more research" 85 | }, { 86 | xpath: t.encAct + '/h:performer/h:assignedEntity', 87 | action: "addAttribute", 88 | params: { 89 | "classCode": "ASSIGNED" 90 | }, 91 | comment: "needs more research" 92 | }, { 93 | xpath: t.payersSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.1.19"]', 94 | action: "removeNode", 95 | comment: "this templateId does not exist in the file" 96 | }, { 97 | xpath: t.policyAct + '/h:performer/h:assignedEntity', 98 | action: "addAttribute", 99 | params: { 100 | "classCode": "ASSIGNED" 101 | }, 102 | comment: "needs more research" 103 | }, { 104 | xpath: t.policyAct + '/h:performer/h:assignedEntity/h:representedOrganization', 105 | action: "addAttribute", 106 | params: { 107 | "classCode": "ORG" 108 | }, 109 | comment: "needs more research" 110 | }, { 111 | xpath: t.pocSection + '/.//h:statusCode[@code="new"]', 112 | action: "removeNode", 113 | comment: "to be researched" 114 | }, { 115 | xpath: t.probSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.5"]', 116 | action: "removeNode", 117 | comment: "this templateId does not exist in the file" 118 | }, { 119 | xpath: t.probStatus + '/h:id', 120 | action: "removeNode", 121 | comment: "to be researched" 122 | }, { 123 | xpath: t.vitalsSection + '/h:templateId[@root="2.16.840.1.113883.10.20.22.2.4"]', 124 | action: "removeNode", 125 | comment: "this templateId does not exist in the file" 126 | }, { 127 | xpath: t.resultsSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.2.3"]', 128 | action: "removeNode", 129 | comment: "this templateId does not exist in the file" 130 | }, { 131 | xpath: t.resultsSection + '/.//h:observationRange[not(*)][not(@*)][not(text())]', 132 | action: "removeNode" 133 | }, { 134 | xpath: t.resultsObs + '/h:value[@xsi:type=\"PQ\"][not(@value)]', 135 | action: "removeNode" 136 | }]; 137 | -------------------------------------------------------------------------------- /lib/entryLevel/payerEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var key = contentModifier.key; 9 | var required = contentModifier.required; 10 | var dataKey = contentModifier.dataKey; 11 | 12 | var policyActivity = { 13 | key: "act", 14 | attributes: { 15 | classCode: "ACT", 16 | moodCode: "EVN" 17 | }, 18 | content: [ 19 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.61"), { 20 | key: "id", 21 | attributes: { 22 | root: leafLevel.inputProperty("identifier"), 23 | extension: leafLevel.inputProperty("extension") 24 | }, 25 | dataKey: 'policy.identifiers', 26 | existsWhen: condition.keyExists('identifier'), 27 | required: true 28 | }, 29 | 30 | { 31 | key: "code", 32 | attributes: leafLevel.code, 33 | dataKey: "policy.code" 34 | }, 35 | fieldLevel.statusCodeCompleted, { 36 | key: "performer", 37 | attributes: { 38 | typeCode: "PRF" 39 | }, 40 | content: [ 41 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.87"), 42 | fieldLevel.assignedEntity 43 | ], 44 | dataKey: "policy.insurance.performer" 45 | }, { 46 | key: "performer", 47 | attributes: { 48 | typeCode: "PRF" 49 | }, 50 | content: [ 51 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.88"), 52 | fieldLevel.assignedEntity 53 | ], 54 | dataKey: "guarantor" 55 | }, { 56 | key: "participant", 57 | attributes: { 58 | typeCode: "COV" 59 | }, 60 | content: [ 61 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.89"), [fieldLevel.effectiveTime, key("time")], { 62 | key: "participantRole", 63 | attributes: { 64 | classCode: "PAT" 65 | }, 66 | content: [ 67 | fieldLevel.id, 68 | fieldLevel.usRealmAddress, 69 | fieldLevel.telecom, { 70 | key: "code", 71 | attributes: leafLevel.code, 72 | dataKey: "code" 73 | }, { 74 | key: "playingEntity", 75 | content: fieldLevel.usRealmName 76 | } 77 | ] 78 | } 79 | ], 80 | dataKey: "participant", 81 | dataTransform: function (input) { 82 | if (input.performer) { 83 | input.identifiers = input.performer.identifiers; 84 | input.address = input.performer.address; 85 | input.phone = input.performer.phone; 86 | } 87 | return input; 88 | } 89 | }, { 90 | key: "participant", 91 | attributes: { 92 | typeCode: "HLD" 93 | }, 94 | content: [ 95 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.90"), { 96 | key: "participantRole", 97 | content: [ 98 | fieldLevel.id, 99 | fieldLevel.usRealmAddress 100 | ], 101 | dataKey: "performer" 102 | } 103 | ], 104 | dataKey: "policy_holder" 105 | }, { 106 | key: "entryRelationship", 107 | attributes: { 108 | typeCode: "REFR" 109 | }, 110 | content: { 111 | key: "act", 112 | attributes: { 113 | classCode: "ACT", 114 | moodCode: "EVN" 115 | }, 116 | content: [ 117 | fieldLevel.templateId("2.16.840.1.113883.10.20.1.19"), 118 | fieldLevel.id, { 119 | key: "entryRelationship", 120 | attributes: { 121 | typeCode: "SUBJ" 122 | }, 123 | content: { 124 | key: "procedure", 125 | attributes: { 126 | classCode: "PROC", 127 | moodCode: "PRMS" 128 | }, 129 | content: { 130 | key: "code", 131 | attributes: leafLevel.code, 132 | dataKey: "code" 133 | } 134 | }, 135 | dataKey: "procedure" 136 | } 137 | ] 138 | }, 139 | dataKey: "authorization" 140 | } 141 | ] 142 | }; 143 | 144 | exports.coverageActivity = { 145 | key: "act", 146 | attributes: { 147 | classCode: "ACT", 148 | moodCode: "EVN" 149 | }, 150 | content: [ 151 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.60"), 152 | fieldLevel.uniqueId, 153 | fieldLevel.id, 154 | fieldLevel.templateCode("CoverageActivity"), 155 | fieldLevel.statusCodeCompleted, { 156 | key: "entryRelationship", 157 | attributes: { 158 | typeCode: "COMP" 159 | }, 160 | content: [ 161 | [policyActivity, required] 162 | ], 163 | required: true 164 | } 165 | ] 166 | }; 167 | -------------------------------------------------------------------------------- /lib/entryLevel/allergyEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require('../condition'); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var sel = require("./sharedEntryLevel"); 9 | 10 | var key = contentModifier.key; 11 | var required = contentModifier.required; 12 | var dataKey = contentModifier.dataKey; 13 | 14 | var allergyStatusObservation = { 15 | key: "observation", 16 | attributes: { 17 | "classCode": "OBS", 18 | "moodCode": "EVN" 19 | }, 20 | content: [ 21 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.28"), 22 | fieldLevel.templateCode("AllergyStatusObservation"), 23 | fieldLevel.statusCodeCompleted, { 24 | key: "value", 25 | attributes: [ 26 | leafLevel.typeCE, 27 | leafLevel.code 28 | ], 29 | existsWhen: condition.codeOrDisplayname, 30 | required: true 31 | } 32 | ], 33 | dataKey: "status" 34 | }; 35 | 36 | var allergyIntoleranceObservation = exports.allergyIntoleranceObservation = { 37 | key: "observation", 38 | attributes: { 39 | "classCode": "OBS", 40 | "moodCode": "EVN", 41 | "negationInd": leafLevel.boolInputProperty("negation_indicator") 42 | }, 43 | content: [ 44 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.7"), 45 | fieldLevel.id, 46 | fieldLevel.templateCode("AllergyObservation"), 47 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 48 | key: "value", 49 | attributes: [ 50 | leafLevel.typeCD, 51 | leafLevel.code 52 | ], 53 | content: { 54 | key: "originalText", 55 | content: { 56 | key: "reference", 57 | attributes: { 58 | "value": leafLevel.nextReference("reaction") 59 | } 60 | } 61 | }, 62 | dataKey: 'intolerance', 63 | existsWhen: condition.codeOrDisplayname, 64 | required: true 65 | }, { 66 | key: "participant", 67 | attributes: { 68 | "typeCode": "CSM" 69 | }, 70 | content: [{ 71 | key: "participantRole", 72 | attributes: { 73 | "classCode": "MANU" 74 | }, 75 | content: [{ 76 | key: "playingEntity", 77 | attributes: { 78 | classCode: "MMAT" 79 | }, 80 | content: [{ 81 | key: "code", 82 | attributes: leafLevel.code, 83 | content: [{ 84 | key: "originalText", 85 | content: [{ 86 | key: "reference", 87 | attributes: { 88 | "value": leafLevel.sameReference("reaction") 89 | } 90 | }] 91 | }, { 92 | key: "translation", 93 | attributes: leafLevel.code, 94 | dataKey: "translations" 95 | }], 96 | require: true 97 | }] 98 | }], 99 | required: true 100 | }], 101 | dataKey: 'allergen' 102 | }, { 103 | key: "entryRelationship", 104 | attributes: { 105 | "typeCode": "SUBJ", 106 | "inversionInd": "true" 107 | }, 108 | content: [ 109 | [allergyStatusObservation, required] 110 | ], 111 | existsWhen: condition.keyExists("status") 112 | }, { 113 | key: "entryRelationship", 114 | attributes: { 115 | "typeCode": "MFST", 116 | "inversionInd": "true" 117 | }, 118 | content: [ 119 | [sel.reactionObservation, required] 120 | ], 121 | dataKey: 'reactions', 122 | existsWhen: condition.keyExists('reaction') 123 | }, { 124 | key: "entryRelationship", 125 | attributes: { 126 | "typeCode": "SUBJ", 127 | "inversionInd": "true" 128 | }, 129 | content: [ 130 | [sel.severityObservation, required] 131 | ], 132 | existsWhen: condition.keyExists('severity') 133 | } 134 | ], 135 | dataKey: "observation", 136 | warning: [ 137 | "negationInd attribute is not specified in specification" 138 | ] 139 | }; 140 | 141 | var allergyProblemAct = exports.allergyProblemAct = { 142 | key: "act", 143 | attributes: { 144 | classCode: "ACT", 145 | moodCode: "EVN" 146 | }, 147 | content: [ 148 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.30"), 149 | fieldLevel.uniqueId, 150 | fieldLevel.id, 151 | fieldLevel.templateCode("AllergyProblemAct"), 152 | fieldLevel.statusCodeActive, [fieldLevel.effectiveTime, required], { 153 | key: "entryRelationship", 154 | attributes: { 155 | typeCode: "SUBJ", 156 | inversionInd: "true" 157 | }, 158 | content: [allergyIntoleranceObservation, required], 159 | existsWhen: condition.keyExists('observation'), 160 | required: true, 161 | warning: "inversionInd is not in spec" 162 | } 163 | ], 164 | warning: "statusCode is not constant in spec" 165 | }; 166 | -------------------------------------------------------------------------------- /lib/engine.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var xmlutil = require('./xmlutil'); 4 | 5 | var expandText = function (input, template) { 6 | var text = template.text; 7 | if (text) { 8 | if (typeof text === 'function') { 9 | text = text(input); 10 | } 11 | if ((text !== null) && (text !== undefined)) { 12 | return text; 13 | } 14 | } 15 | return null; 16 | }; 17 | 18 | var expandAttributes = function expandAttributes(input, context, attrObj, attrs) { 19 | if (Array.isArray(attrObj)) { 20 | attrObj.forEach(function (attrObjElem) { 21 | expandAttributes(input, context, attrObjElem, attrs); 22 | }); 23 | } else if (typeof attrObj === 'function') { 24 | expandAttributes(input, context, attrObj(input, context), attrs); 25 | } else { 26 | Object.keys(attrObj).forEach(function (attrKey) { 27 | var attrVal = attrObj[attrKey]; 28 | if (typeof attrVal === 'function') { 29 | attrVal = attrVal(input, context); 30 | } 31 | if ((attrVal !== null) && (attrVal !== undefined)) { 32 | attrs[attrKey] = attrVal; 33 | } 34 | }); 35 | } 36 | }; 37 | 38 | var fillAttributes = function (node, input, context, template) { 39 | var attrObj = template.attributes; 40 | if (attrObj) { 41 | var inputAttrKey = template.attributeKey; 42 | if (inputAttrKey) { 43 | input = input[inputAttrKey]; 44 | } 45 | if (input) { 46 | var attrs = {}; 47 | expandAttributes(input, context, attrObj, attrs); 48 | xmlutil.nodeAttr(node, attrs); 49 | } 50 | } 51 | }; 52 | 53 | var update; 54 | 55 | var fillContent = function (node, input, context, template) { 56 | var content = template.content; 57 | if (content) { 58 | if (!Array.isArray(content)) { 59 | content = [content]; 60 | } 61 | content.forEach(function (element) { 62 | if (Array.isArray(element)) { 63 | var actualElement = Object.create(element[0]); 64 | for (var i = 1; i < element.length; ++i) { 65 | element[i](actualElement); 66 | } 67 | update(node, input, context, actualElement); 68 | } else { 69 | update(node, input, context, element); 70 | } 71 | }); 72 | } 73 | }; 74 | 75 | var updateUsingTemplate = function updateUsingTemplate(xmlDoc, input, context, template) { 76 | var condition = template.existsWhen; 77 | if ((!condition) || condition(input, context)) { 78 | var name = template.key; 79 | var text = expandText(input, template); 80 | if (((text !== null) && (text !== undefined)) || template.content || template.attributes) { 81 | var node = xmlutil.newNode(xmlDoc, name, text); 82 | 83 | fillAttributes(node, input, context, template); 84 | fillContent(node, input, context, template); 85 | return true; 86 | } 87 | } 88 | return false; 89 | }; 90 | 91 | var transformInput = function (input, template) { 92 | var inputKey = template.dataKey; 93 | if (inputKey) { 94 | var pieces = inputKey.split('.'); 95 | pieces.forEach(function (piece) { 96 | if (Array.isArray(input) && (piece !== "0")) { 97 | var nextInputs = []; 98 | input.forEach(function (inputElement) { 99 | var nextInput = inputElement[piece]; 100 | if (nextInput) { 101 | if (Array.isArray(nextInput)) { 102 | nextInput.forEach(function (nextInputElement) { 103 | if (nextInputElement) { 104 | nextInputs.push(nextInputElement); 105 | } 106 | }); 107 | } else { 108 | nextInputs.push(nextInput); 109 | } 110 | } 111 | }); 112 | if (nextInputs.length === 0) { 113 | input = null; 114 | } else { 115 | input = nextInputs; 116 | } 117 | } else { 118 | input = input && input[piece]; 119 | } 120 | }); 121 | } 122 | if (input) { 123 | var transform = template.dataTransform; 124 | if (transform) { 125 | input = transform(input); 126 | } 127 | } 128 | return input; 129 | }; 130 | 131 | var update = exports.update = function (xmlDoc, input, context, template) { 132 | var filled = false; 133 | if (input) { 134 | input = transformInput(input, template); 135 | if (input) { 136 | if (Array.isArray(input)) { 137 | input.forEach(function (element) { 138 | filled = updateUsingTemplate(xmlDoc, element, context, template) || filled; 139 | }); 140 | } else { 141 | filled = updateUsingTemplate(xmlDoc, input, context, template); 142 | } 143 | } 144 | } 145 | if ((!filled) && template.required && !context.preventNullFlavor) { 146 | var node = xmlutil.newNode(xmlDoc, template.key); 147 | xmlutil.nodeAttr(node, { 148 | nullFlavor: 'UNK' 149 | }); 150 | } 151 | }; 152 | 153 | exports.create = function (template, input, context) { 154 | var doc = new xmlutil.newDocument(); 155 | update(doc, input, context, template); 156 | var result = xmlutil.serializeToString(doc); 157 | return result; 158 | }; 159 | -------------------------------------------------------------------------------- /test/xmlmods/ccd1Parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | var titleMap = { 6 | "ALLERGIES, ADVERSE REACTIONS, ALERTS": "Allergies, adverse reactions, alerts", 7 | "MEDICATIONS": "History of medication use", 8 | "IMMUNIZATIONS": "Immunizations", 9 | "PROCEDURES": "History of Procedures", 10 | "ENCOUNTERS": "Encounters", 11 | "INSURANCE PROVIDERS": "Payers", 12 | "PLAN OF CARE": "Plan of Care", 13 | "PROBLEMS": "Problem List", 14 | "SOCIAL HISTORY": "Social History", 15 | "VITAL SIGNS": "Vital Signs", 16 | "RESULTS": "Relevant diagnostic tests and/or laboratory data" 17 | }; 18 | 19 | var normalizedCodeSystemNames = { 20 | "RxNorm": "RXNORM", 21 | "SNOMED-CT": "SNOMED CT", 22 | "NCI Thesaurus": "Medication Route FDA", 23 | "National Cancer Institute (NCI) Thesaurus": "Medication Route FDA", 24 | "HL7 ActNoImmunizationReason": "Act Reason", 25 | "HL7 ActEncounterCode": "ActCode", 26 | "HL7 RoleClassRelationship": "HL7 RoleCode", 27 | "HL7 Role code": "HL7 Role", 28 | "HL7 RoleCode": { 29 | src: "2.16.840.1.113883.5.111", 30 | value: "HL7 Role" 31 | }, 32 | "MaritalStatusCode": "HL7 Marital Status", 33 | "Race & Ethnicity - CDC": "Race and Ethnicity - CDC" 34 | }; 35 | 36 | var normalizedDisplayNames = { 37 | "HISTORY OF MEDICATION USE": "History of medication use", 38 | "History of immunizations": "Immunizations", 39 | "Patient Objection": "Patient objection", 40 | "HISTORY OF PROCEDURES": "History of Procedures", 41 | "History of encounters": "Encounters", 42 | "Payer": "Payers", 43 | "Treatment plan": "Plan of Care", 44 | "PROBLEM LIST": "Problem List", 45 | "VITAL SIGNS": "Vital Signs", 46 | "RESULTS": "Relevant diagnostic tests and/or laboratory data" 47 | }; 48 | 49 | module.exports = [{ 50 | xpath: "//h:title", 51 | action: "replaceText", 52 | params: titleMap, 53 | comment: "titles may differ" 54 | }, { 55 | xpath: t.immSection + '/.//h:effectiveTime[@xsi:type="IVL_TS"]', 56 | action: "removeAttribute", 57 | params: "type" 58 | }, { 59 | xpath: t.immInstructions, 60 | action: "addAttribute", 61 | params: { 62 | "inversionInd": "true" 63 | }, 64 | comment: "erroneous in the sample file" 65 | }, { 66 | xpath: t.medSection + '/.//h:effectiveTime[@xsi:type="IVL_TS"]', 67 | action: "removeAttribute", 68 | params: "type" 69 | }, { 70 | xpath: "//*[@codeSystem][@codeSystemName]", 71 | action: "normalize", 72 | params: { 73 | attr: "codeSystemName", 74 | srcAttr: "codeSystem", 75 | map: normalizedCodeSystemNames 76 | }, 77 | comment: 'blue-button parser normalization' 78 | }, { 79 | xpath: "//*[@codeSystem][@displayName][@code]", 80 | action: "normalize", 81 | params: { 82 | attr: "displayName", 83 | srcAttr: "code", 84 | map: normalizedDisplayNames 85 | }, 86 | comment: 'blue-button parser normalization' 87 | }, { 88 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.5.1\"]", 89 | action: "addAttributeWhenEmpty", 90 | params: { 91 | "codeSystemName": "HL7 AdministrativeGender" 92 | } 93 | }, { 94 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.6.96\"]", 95 | action: "addAttributeWhenEmpty", 96 | params: { 97 | "codeSystemName": "SNOMED CT" 98 | } 99 | }, { 100 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.6.1\"]", 101 | action: "addAttributeWhenEmpty", 102 | params: { 103 | "codeSystemName": "LOINC" 104 | } 105 | }, { 106 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.6.88\"]", 107 | action: "addAttributeWhenEmpty", 108 | params: { 109 | "codeSystemName": "RXNORM" 110 | } 111 | }, { 112 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.5.6\"]", 113 | action: "addAttributeWhenEmpty", 114 | params: { 115 | "codeSystemName": "HL7ActClass" 116 | } 117 | }, { 118 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.5.111\"]", 119 | action: "addAttributeWhenEmpty", 120 | params: { 121 | "codeSystemName": "HL7 Role" 122 | } 123 | }, { 124 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.5.83\"][@code=\"N\"]", 125 | action: "addAttributeWhenEmpty", 126 | params: { 127 | "codeSystemName": "ObservationInterpretation", 128 | "displayName": "Normal" 129 | } 130 | }, { 131 | xpath: "//*[@codeSystem=\"2.16.840.1.113883.5.83\"][@code=\"L\"]", 132 | action: "addAttributeWhenEmpty", 133 | params: { 134 | "codeSystemName": "ObservationInterpretation", 135 | "displayName": "Low" 136 | } 137 | }, { 138 | xpath: t.allergyObs + "/h:code[@codeSystem=\"2.16.840.1.113883.5.4\"][@code=\"ASSERTION\"]", 139 | action: "addAttributeWhenEmpty", 140 | params: { 141 | "codeSystemName": "ActCode", 142 | "displayName": "Assertion" 143 | } 144 | }, { 145 | xpath: t.socHistObs + "/h:code[@codeSystem=\"2.16.840.1.113883.5.4\"][@code=\"ASSERTION\"]", 146 | action: "addAttributeWhenEmpty", 147 | params: { 148 | "codeSystemName": "ActCode", 149 | "displayName": "Assertion" 150 | } 151 | }, { 152 | xpath: t.allergiesSection + "/h:code[@code=\"48765-2\"]", 153 | action: "addAttributeWhenEmpty", 154 | params: { 155 | "displayName": "Allergies, adverse reactions, alerts" 156 | } 157 | }]; 158 | -------------------------------------------------------------------------------- /lib/entryLevel/immunizationEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var sharedEntryLevel = require("./sharedEntryLevel"); 9 | 10 | var key = contentModifier.key; 11 | var required = contentModifier.required; 12 | var dataKey = contentModifier.dataKey; 13 | 14 | var immunizationMedicationInformation = { 15 | key: "manufacturedProduct", 16 | attributes: { 17 | classCode: "MANU" 18 | }, 19 | content: [ 20 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.54"), 21 | fieldLevel.id, { 22 | key: "manufacturedMaterial", 23 | content: [{ 24 | key: "code", 25 | attributes: leafLevel.code, 26 | content: [{ 27 | key: "originalText", 28 | text: leafLevel.inputProperty("unencoded_name"), 29 | content: { 30 | key: "reference", 31 | attributes: { 32 | "value": leafLevel.nextReference("imminfo") 33 | } 34 | } 35 | }, { 36 | key: "translation", 37 | attributes: leafLevel.code, 38 | dataKey: "translations" 39 | }] 40 | }, { 41 | key: "lotNumberText", 42 | text: leafLevel.input, 43 | dataKey: "lot_number" 44 | }], 45 | dataKey: "product", 46 | required: true 47 | }, { 48 | key: "manufacturerOrganization", 49 | content: { 50 | key: "name", 51 | text: leafLevel.input, 52 | }, 53 | dataKey: "manufacturer" 54 | } 55 | ], 56 | dataTransform: function (input) { 57 | if (input.product) { 58 | input.product.lot_number = input.lot_number; 59 | } 60 | return input; 61 | } 62 | }; 63 | 64 | var immunizationRefusalReason = { 65 | key: "observation", 66 | attributes: { 67 | classCode: "OBS", 68 | moodCode: "EVN" 69 | }, 70 | content: [ 71 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.53"), 72 | fieldLevel.id, { 73 | key: "code", 74 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.8"), 75 | required: true 76 | }, 77 | fieldLevel.statusCodeCompleted 78 | ] 79 | }; 80 | 81 | var immunizationActivityAttributes = function (input) { 82 | if (input.status) { 83 | if (input.status === "refused") { 84 | return { 85 | moodCode: "EVN", 86 | negationInd: "true" 87 | }; 88 | } 89 | if (input.status === "pending") { 90 | return { 91 | moodCode: "INT", 92 | negationInd: "false" 93 | }; 94 | } 95 | if (input.status === "complete") { 96 | return { 97 | moodCode: "EVN", 98 | negationInd: "false" 99 | }; 100 | } 101 | } 102 | return null; 103 | }; 104 | 105 | exports.immunizationActivity = { 106 | key: "substanceAdministration", 107 | attributes: [{ 108 | classCode: "SBADM" 109 | }, immunizationActivityAttributes], 110 | content: [ 111 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.52"), 112 | fieldLevel.uniqueId, 113 | fieldLevel.id, 114 | fieldLevel.text(leafLevel.nextReference("immunization")), 115 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 116 | key: "repeatNumber", 117 | attributes: { 118 | value: leafLevel.inputProperty("sequence_number") 119 | }, 120 | existsWhen: function (input) { 121 | return input.sequence_number || (input.sequence_number === ""); 122 | } 123 | }, { 124 | key: "routeCode", 125 | attributes: leafLevel.code, 126 | dataKey: "administration.route" 127 | }, { 128 | key: "approachSiteCode", 129 | attributes: leafLevel.code, 130 | dataKey: "administration.body_site" 131 | }, { 132 | key: "doseQuantity", 133 | attributes: { 134 | value: leafLevel.inputProperty("value"), 135 | unit: leafLevel.inputProperty("unit") 136 | }, 137 | dataKey: "administration.dose" 138 | }, { 139 | key: "consumable", 140 | content: [ 141 | [immunizationMedicationInformation, required] 142 | ], 143 | dataKey: "product", 144 | required: true 145 | }, 146 | fieldLevel.performer, { 147 | key: "entryRelationship", 148 | attributes: { 149 | typeCode: "SUBJ", 150 | inversionInd: "true" 151 | }, 152 | content: [sharedEntryLevel.instructions, required], 153 | dataKey: "instructions" 154 | }, { 155 | key: "entryRelationship", 156 | attributes: { 157 | typeCode: "RSON" 158 | }, 159 | content: [immunizationRefusalReason, required], 160 | dataKey: "refusal_reason" 161 | } 162 | ], 163 | notImplemented: [ 164 | "code", 165 | "administrationUnitCode", 166 | "participant:drugVehicle", 167 | "entryRelationship:indication", 168 | "entryRelationship:medicationSupplyOrder", 169 | "entryRelationship:medicationDispense", 170 | "entryRelationship:reactionObservation", 171 | "entryRelationship:preconditionForSubstanceAdministration" 172 | ] 173 | }; 174 | -------------------------------------------------------------------------------- /lib/entryLevel/problemEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var sharedEntryLevel = require("./sharedEntryLevel"); 9 | 10 | var key = contentModifier.key; 11 | var required = contentModifier.required; 12 | var dataKey = contentModifier.dataKey; 13 | 14 | var problemStatus = { 15 | key: "observation", 16 | attributes: { 17 | classCode: "OBS", 18 | moodCode: "EVN" 19 | }, 20 | content: [ 21 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.6"), 22 | fieldLevel.id, 23 | fieldLevel.templateCode("ProblemStatus"), 24 | fieldLevel.statusCodeCompleted, 25 | fieldLevel.effectiveTime, { 26 | key: "value", 27 | attributes: [{ 28 | "xsi:type": "CD" 29 | }, 30 | leafLevel.codeFromName("2.16.840.1.113883.3.88.12.80.68") 31 | ], 32 | dataKey: "name", 33 | required: true 34 | } 35 | ], 36 | warning: "effectiveTime does not exist in the specification" 37 | }; 38 | 39 | var healthStatusObservation = { 40 | key: "observation", 41 | attributes: { 42 | classCode: "OBS", 43 | moodCode: "EVN" 44 | }, 45 | content: [ 46 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.5"), 47 | fieldLevel.templateCode("HealthStatusObservation"), 48 | fieldLevel.text(leafLevel.nextReference("healthStatus")), 49 | fieldLevel.statusCodeCompleted, { 50 | key: "value", 51 | attributes: { 52 | "xsi:type": "CD", 53 | code: "81323004", 54 | codeSystem: "2.16.840.1.113883.6.96", 55 | codeSystemName: "SNOMED CT", 56 | displayName: leafLevel.inputProperty("patient_status") 57 | }, 58 | required: true, 59 | toDo: "The attribute should not be constant" 60 | } 61 | ] 62 | }; 63 | 64 | var problemObservation = { 65 | key: "observation", 66 | attributes: { 67 | classCode: "OBS", 68 | moodCode: "EVN", 69 | negationInd: leafLevel.boolInputProperty("negation_indicator") 70 | }, 71 | content: [ 72 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.4"), 73 | fieldLevel.id, { 74 | key: "code", 75 | attributes: { 76 | nullFlavor: "UNK" 77 | } 78 | }, 79 | fieldLevel.text(leafLevel.nextReference("problem")), 80 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, dataKey("problem.date_time")], { 81 | key: "value", 82 | attributes: [{ 83 | "xsi:type": "CD" 84 | }, 85 | leafLevel.code 86 | ], 87 | content: [{ 88 | key: "translation", 89 | attributes: leafLevel.code, 90 | dataKey: "translations" 91 | }], 92 | dataKey: "problem.code", 93 | existsWhen: condition.codeOrDisplayname, 94 | required: true 95 | }, { 96 | key: "entryRelationship", 97 | attributes: { 98 | typeCode: "REFR" 99 | }, 100 | content: [ 101 | [problemStatus, required] 102 | ], 103 | dataTransform: function (input) { 104 | if (input && input.status) { 105 | var result = input.status; 106 | result.identifiers = input.identifiers; 107 | return result; 108 | } 109 | return null; 110 | } 111 | }, { 112 | key: "entryRelationship", 113 | attributes: { 114 | typeCode: "SUBJ", 115 | inversionInd: "true" 116 | }, 117 | content: [ 118 | [sharedEntryLevel.ageObservation, required] 119 | ], 120 | existsWhen: condition.keyExists("onset_age") 121 | }, { 122 | key: "entryRelationship", 123 | attributes: { 124 | typeCode: "REFR" 125 | }, 126 | content: [ 127 | [healthStatusObservation, required] 128 | ], 129 | existsWhen: condition.keyExists("patient_status") 130 | }, { 131 | key: "entryRelationship", 132 | attributes: { 133 | "typeCode": "SUBJ", 134 | "inversionInd": "true" 135 | }, 136 | content: [ 137 | [sharedEntryLevel.severityObservation] 138 | ], 139 | dataKey: "problem", 140 | existsWhen: condition.keyExists("severity") 141 | } 142 | ], 143 | notImplemented: [ 144 | "code" 145 | ] 146 | }; 147 | 148 | exports.problemConcernAct = { 149 | key: "act", 150 | attributes: { 151 | classCode: "ACT", 152 | moodCode: "EVN" 153 | }, 154 | content: [ 155 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.3"), 156 | fieldLevel.uniqueId, { 157 | key: "id", 158 | attributes: { 159 | root: leafLevel.inputProperty("identifier"), 160 | extension: leafLevel.inputProperty("extension") 161 | }, 162 | dataKey: 'source_list_identifiers', 163 | existsWhen: condition.keyExists('identifier'), 164 | required: true 165 | }, 166 | fieldLevel.templateCode("ProblemConcernAct"), 167 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 168 | key: "entryRelationship", 169 | attributes: { 170 | typeCode: "SUBJ" 171 | }, 172 | content: [ 173 | [problemObservation, required] 174 | ], 175 | required: true 176 | } 177 | ] 178 | }; 179 | -------------------------------------------------------------------------------- /test/sample_runs/test-gen-parse-gen.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require('path'); 3 | var bb = require('@amida-tech/blue-button'); 4 | var bbg = require('../../index'); 5 | 6 | describe('parse generate parse generate', function () { 7 | var generatedDir = null; 8 | 9 | beforeAll(function () { 10 | generatedDir = path.join(__dirname, "../fixtures/files/generated"); 11 | expect(generatedDir).toBeDefined(); 12 | }); 13 | 14 | it('CCD_1 should still be same', function () { 15 | var data = fs.readFileSync(__dirname + "/../fixtures/files/ccda_xml/CCD_1.xml").toString('utf8'); 16 | var result = bb.parseString(data); 17 | 18 | // check validation 19 | var val = bb.validator.validateDocumentModel(result); 20 | expect(val).toBe(true); 21 | 22 | // generate ccda 23 | var xml = bbg.generateCCD(result); 24 | 25 | // parse generated ccda 26 | var result2 = bb.parseString(xml); 27 | var val2 = bb.validator.validateDocumentModel(result2); 28 | expect(val2).toBe(true); 29 | 30 | delete result.errors; 31 | delete result2.errors; 32 | 33 | expect(result2).toEqual(result); 34 | }); 35 | 36 | it('Vitera_CCDA_SMART_Sample.xml should still be same', function () { 37 | var data = fs.readFileSync(__dirname + "/../fixtures/files/ccda_xml/Vitera_CCDA_SMART_Sample.xml").toString(); 38 | var result = bb.parseString(data); 39 | 40 | // check validation 41 | var val = bb.validator.validateDocumentModel(result); 42 | 43 | // generate ccda 44 | var xml = bbg.generateCCD(result); 45 | 46 | // parse generated ccda 47 | var result2 = bb.parseString(xml); 48 | 49 | // re-generate 50 | var xml2 = bbg.generateCCD(result2); 51 | 52 | delete result.errors; 53 | delete result2.errors; 54 | 55 | expect(result2).toEqual(result); 56 | }); 57 | 58 | it('VA_CCD_Sample_File_Version_12_5_1.xml should still be same', function () { 59 | var data = fs.readFileSync(__dirname + "/../fixtures/files/ccda_xml/VA_CCD_Sample_File_Version_12_5_1.xml").toString(); 60 | var result = bb.parseString(data); 61 | result.meta.sections.sort(); 62 | 63 | // check validation 64 | var val = bb.validator.validateDocumentModel(result); 65 | 66 | // generate ccda 67 | var xml = bbg.generateCCD(result); 68 | 69 | // parse generated ccda 70 | var result2 = bb.parseString(xml); 71 | result2.meta.sections.sort(); 72 | 73 | // re-generate 74 | var xml2 = bbg.generateCCD(result2); 75 | 76 | delete result.errors; 77 | delete result2.errors; 78 | 79 | expect(result2).toEqual(result); 80 | }); 81 | 82 | it('SampleCCDDocument.xml should still be same', function () { 83 | var data = fs.readFileSync(__dirname + "/../fixtures/files/ccda_xml/SampleCCDDocument.xml").toString(); 84 | var result = bb.parseString(data); 85 | result.meta.sections.sort(); 86 | 87 | // check validation 88 | var val = bb.validator.validateDocumentModel(result); 89 | 90 | // generate ccda 91 | var xml = bbg.generateCCD(result); 92 | 93 | // parse generated ccda 94 | var result2 = bb.parseString(xml); 95 | result2.meta.sections.sort(); 96 | 97 | // re-generate 98 | var xml2 = bbg.generateCCD(result2); 99 | 100 | delete result.errors; 101 | delete result2.errors; 102 | delete result.data.providers; 103 | result.meta.sections = result.meta.sections.filter(function (v) { 104 | return v !== 'providers'; 105 | }); 106 | 107 | expect(result2).toEqual(result); 108 | }); 109 | 110 | it('cms_sample.xml should not crash', function () { 111 | var data = fs.readFileSync(__dirname + "/../fixtures/files/cms_txt/cms_sample.txt").toString(); 112 | var result = bb.parseText(data); 113 | 114 | // check validation 115 | var val = bb.validator.validateDocumentModel(result); 116 | 117 | // generate ccda 118 | var xml = bbg.generateCCD(result); 119 | 120 | // parse generated ccda 121 | var result2 = bb.parseString(xml); 122 | 123 | // re-generate 124 | var xml2 = bbg.generateCCD(result2); 125 | 126 | delete result.errors; 127 | delete result2.errors; 128 | delete result.data.claims; 129 | delete result2.data.claims; 130 | delete result.data.plan_of_care; 131 | delete result2.data.plan_of_care; 132 | delete result.data.providers; 133 | delete result2.data.providers; 134 | 135 | expect(result2.data).toEqual(result.data); 136 | }); 137 | 138 | it('skewed sample data from app should still be same', function () { 139 | //var data = fs.readFileSync("./sample.JSON").toString(); 140 | 141 | var data = fs.readFileSync(__dirname + "/../fixtures/json/sample.JSON"); 142 | 143 | //convert string into JSON 144 | var result = JSON.parse(data); 145 | 146 | // check validation 147 | var val = bb.validator.validateDocumentModel(result); 148 | 149 | // generate ccda 150 | 151 | delete result.errors; 152 | var input = { 153 | data: result 154 | }; 155 | var xml = bbg.generateCCD(input, { 156 | preventNullFlavor: true 157 | }); 158 | 159 | // parse generated ccda 160 | var result2 = bb.parseString(xml); 161 | 162 | // re-generate 163 | var xml2 = bbg.generateCCD(result2); 164 | 165 | delete result2.errors; 166 | //assert.deepEqual(result2.data, result); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/xmlmods/templatePath.js: -------------------------------------------------------------------------------- 1 | var medSection = exports.medSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.1" or @root="2.16.840.1.113883.10.20.22.2.1.1"]/..'; 2 | var medActivity = exports.medActivity = medSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.16"]/..'; 3 | var medSupplyOrder = exports.medSupplyOrder = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.17"]/..'; 4 | var medDispense = exports.medDispense = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.18"]/..'; 5 | exports.medActivityInstructions = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.20"]/../..'; 6 | exports.medStatus = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.1.47"]/../..'; 7 | exports.medProbAct = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.3"]/../..'; 8 | exports.medDispenseInfo = medDispense + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.23"]/../..'; 9 | exports.medSupplyInfo = medSupplyOrder + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.23"]/../..'; 10 | exports.medActivityInfo = medActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.23"]/..'; 11 | 12 | var immSection = exports.immSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.2" or @root="2.16.840.1.113883.10.20.22.2.2.1"]/..'; 13 | var immActivity = exports.immActivity = immSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.52"]/..'; 14 | exports.immRefusalReason = immActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.53"]/..'; 15 | exports.immActUnknown1 = immActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.1.46"]/../..'; 16 | exports.immActUnknown2 = immActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.1.47"]/../..'; 17 | exports.immActComment = immActivity + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.64"]/../..'; 18 | exports.immInstructions = immSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.20"]/../..'; 19 | 20 | var procSection = exports.procSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.7" or @root="2.16.840.1.113883.10.20.22.2.7.1"]/..'; 21 | var procActProc = exports.procActProc = procSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.14"]/..'; 22 | exports.procActProcUnknown = procActProc + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.4"]/../..'; 23 | exports.procProductInstance = procActProc + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.37"]/../..'; 24 | exports.procActEither = procSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.12" or @root="2.16.840.1.113883.10.20.22.4.13" or @root="2.16.840.1.113883.10.20.22.4.14"]/..'; 25 | 26 | var payersSection = exports.payersSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.18"]/..'; 27 | var coverageAct = exports.coverageAct = payersSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.60"]/..'; 28 | var policyAct = exports.policyAct = coverageAct + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.61"]/..'; 29 | 30 | var probSection = exports.probSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.5" or @root="2.16.840.1.113883.10.20.22.2.5.1"]/..'; 31 | var probAct = exports.probAct = probSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.3"]/..'; 32 | var probObservation = exports.probObservation = probAct + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.4"]/..'; 33 | var probStatus = exports.probStatus = probObservation + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.6"]/..'; 34 | exports.probActComment = probAct + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.64"]/../..'; 35 | 36 | var allergiesSection = exports.allergiesSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.6.1"]/..'; 37 | var allergyObs = exports.allergyObs = allergiesSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.7"]/..'; 38 | var allergyReaction = exports.allergyReaction = allergyObs + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.9"]/..'; 39 | exports.allergyCommentAct = allergiesSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.64"]/../..'; 40 | 41 | var encSection = exports.encSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.22"]/..'; 42 | var encAct = exports.encAct = encSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.49"]/..'; 43 | 44 | var pocSection = exports.pocSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.10"]/..'; 45 | var pocActProc = exports.pocActProc = pocSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.41"]/..'; 46 | var pocActProcUnknown = exports.pocActProcUnknown = pocActProc + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.4"]/../..'; 47 | 48 | var resultsSection = exports.resultsSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.3.1"]/..'; 49 | var resultOrg = exports.resultOrg = resultsSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.1"]/..'; 50 | var resultObs = exports.resultObs = resultOrg + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.2"]/..'; 51 | exports.resultsCommentAct = resultsSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.64"]/../..'; 52 | 53 | var vitalsSection = exports.vitalsSection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.4" or @root="2.16.840.1.113883.10.20.22.2.4.1"]/..'; 54 | var vitalsObs = exports.vitalsObs = vitalsSection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.27"]/..'; 55 | 56 | var socialHistorySection = '//h:templateId[@root="2.16.840.1.113883.10.20.22.2.17"]/..'; 57 | exports.socHistObs = socialHistorySection + '/.//h:templateId[@root="2.16.840.1.113883.10.20.22.4.78"]/..'; 58 | -------------------------------------------------------------------------------- /lib/entryLevel/planOfCareEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var key = contentModifier.key; 9 | var required = contentModifier.required; 10 | var dataKey = contentModifier.dataKey; 11 | 12 | exports.planOfCareActivityAct = { 13 | key: "act", 14 | attributes: { 15 | classCode: "ACT", 16 | moodCode: "RQO" 17 | }, 18 | content: [ 19 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.39"), 20 | fieldLevel.uniqueId, 21 | fieldLevel.id, { 22 | key: "code", 23 | attributes: leafLevel.code, 24 | dataKey: "plan" 25 | }, 26 | fieldLevel.statusCodeNew, 27 | fieldLevel.effectiveTime 28 | ], 29 | existsWhen: function (input) { 30 | return input.type === "act"; 31 | } 32 | }; 33 | 34 | exports.planOfCareActivityObservation = { 35 | key: "observation", 36 | attributes: { 37 | classCode: "OBS", 38 | moodCode: "RQO" 39 | }, 40 | content: [ 41 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.44"), 42 | fieldLevel.uniqueId, 43 | fieldLevel.id, { 44 | key: "code", 45 | attributes: leafLevel.code, 46 | dataKey: "plan" 47 | }, 48 | fieldLevel.statusCodeNew, 49 | fieldLevel.effectiveTime 50 | ], 51 | existsWhen: function (input) { 52 | return input.type === "observation"; 53 | } 54 | }; 55 | 56 | exports.planOfCareActivityProcedure = { 57 | key: "procedure", 58 | attributes: { 59 | classCode: "PROC", 60 | moodCode: "RQO" 61 | }, 62 | content: [ 63 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.41"), 64 | fieldLevel.uniqueId, 65 | fieldLevel.id, { 66 | key: "code", 67 | attributes: leafLevel.code, 68 | dataKey: "plan" 69 | }, 70 | fieldLevel.statusCodeNew, 71 | fieldLevel.effectiveTime 72 | ], 73 | existsWhen: function (input) { 74 | return input.type === "procedure"; 75 | } 76 | }; 77 | 78 | exports.planOfCareActivityEncounter = { 79 | key: "encounter", 80 | attributes: { 81 | classCode: "ENC", 82 | moodCode: "INT" 83 | }, 84 | content: [ 85 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.40"), 86 | fieldLevel.uniqueId, 87 | fieldLevel.id, { 88 | key: "code", 89 | attributes: leafLevel.code, 90 | dataKey: "plan" 91 | }, 92 | fieldLevel.statusCodeNew, 93 | fieldLevel.effectiveTime 94 | ], 95 | existsWhen: function (input) { 96 | return input.type === "encounter"; 97 | } 98 | }; 99 | 100 | exports.planOfCareActivitySubstanceAdministration = { 101 | key: "substanceAdministration", 102 | attributes: { 103 | classCode: "SBADM", 104 | moodCode: "RQO" 105 | }, 106 | content: [ 107 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.42"), 108 | fieldLevel.uniqueId, 109 | fieldLevel.id, { 110 | key: "code", 111 | attributes: leafLevel.code, 112 | dataKey: "plan" 113 | }, 114 | fieldLevel.statusCodeNew, 115 | fieldLevel.effectiveTime 116 | ], 117 | existsWhen: function (input) { 118 | return input.type === "substanceAdministration"; 119 | } 120 | }; 121 | 122 | exports.planOfCareActivitySupply = { 123 | key: "supply", 124 | attributes: { 125 | classCode: "SPLY", 126 | moodCode: "INT" 127 | }, 128 | content: [ 129 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.43"), 130 | fieldLevel.uniqueId, 131 | fieldLevel.id, { 132 | key: "code", 133 | attributes: leafLevel.code, 134 | dataKey: "plan" 135 | }, 136 | fieldLevel.statusCodeNew, 137 | fieldLevel.effectiveTime 138 | ], 139 | existsWhen: function (input) { 140 | return input.type === "supply"; 141 | } 142 | }; 143 | 144 | var goal = { 145 | 146 | key: "code", 147 | attributes: { 148 | "code": leafLevel.deepInputProperty("code"), 149 | "displayName": "Goal" 150 | }, 151 | content: [{ 152 | key: "originalText", 153 | text: leafLevel.deepInputProperty("name") 154 | }], 155 | dataKey: "goal" 156 | }; 157 | 158 | var intervention = { 159 | 160 | key: "code", 161 | attributes: { 162 | "code": leafLevel.deepInputProperty("code"), 163 | "displayName": "Intervention" 164 | }, 165 | content: [{ 166 | key: "originalText", 167 | text: leafLevel.deepInputProperty("name") 168 | }], 169 | dataKey: "intervention" 170 | }; 171 | 172 | exports.planOfCareActivityInstructions = { 173 | key: "instructions", 174 | attributes: { 175 | classCode: "ACT", 176 | moodCode: "INT" 177 | }, 178 | content: [ 179 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.20"), 180 | fieldLevel.uniqueId, 181 | fieldLevel.id, { 182 | key: "code", 183 | attributes: leafLevel.code, 184 | dataKey: "plan" 185 | }, 186 | fieldLevel.statusCodeNew, { 187 | key: "priorityCode", 188 | attributes: { 189 | "code": leafLevel.deepInputProperty("code"), 190 | "displayName": "Severity Code" 191 | }, 192 | content: [{ 193 | key: "originalText", 194 | text: leafLevel.deepInputProperty("name") 195 | }], 196 | dataKey: "severity" 197 | }, 198 | fieldLevel.effectiveTime, { 199 | key: "entryRelationship", 200 | attributes: { 201 | typeCode: "COMP" 202 | }, 203 | content: [{ 204 | key: "observation", 205 | attributes: { 206 | classCode: "OBS", 207 | moodCode: "GOL" 208 | }, 209 | content: [fieldLevel.effectiveTime, goal, { 210 | key: "act", 211 | attributes: { 212 | classCode: "ACT", 213 | moodCode: "INT" 214 | }, 215 | 216 | content: [{ 217 | key: "entryRelationship", 218 | attributes: { 219 | typeCode: "REFR" 220 | }, 221 | content: [intervention], 222 | dataKey: "interventions" 223 | }] 224 | }], 225 | dataKey: "goals" 226 | 227 | }], 228 | required: true 229 | } 230 | ], 231 | existsWhen: function (input) { 232 | return input.type === "instructions"; 233 | } 234 | }; 235 | -------------------------------------------------------------------------------- /lib/entryLevel/sharedEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require('../condition'); 6 | 7 | var severityObservation = exports.severityObservation = { 8 | key: "observation", 9 | attributes: { 10 | "classCode": "OBS", 11 | "moodCode": "EVN" 12 | }, 13 | content: [ 14 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.8"), 15 | fieldLevel.templateCode("SeverityObservation"), 16 | fieldLevel.text(leafLevel.nextReference("severity")), 17 | fieldLevel.statusCodeCompleted, { 18 | key: "value", 19 | attributes: [ 20 | leafLevel.typeCD, 21 | leafLevel.code 22 | ], 23 | dataKey: "code", 24 | existsWhen: condition.codeOrDisplayname, 25 | required: true 26 | }, { 27 | key: "interpretationCode", 28 | attributes: leafLevel.code, 29 | dataKey: "interpretation", 30 | existsWhen: condition.codeOrDisplayname 31 | } 32 | ], 33 | dataKey: "severity", 34 | existsWhen: condition.keyExists("code") 35 | }; 36 | 37 | var reactionObservation = exports.reactionObservation = { 38 | key: "observation", 39 | attributes: { 40 | "classCode": "OBS", 41 | "moodCode": "EVN" 42 | }, 43 | content: [ 44 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.9"), 45 | fieldLevel.id, 46 | fieldLevel.nullFlavor("code"), 47 | fieldLevel.text(leafLevel.sameReference("reaction")), 48 | fieldLevel.statusCodeCompleted, 49 | fieldLevel.effectiveTime, { 50 | key: "value", 51 | attributes: [ 52 | leafLevel.typeCD, 53 | leafLevel.code 54 | ], 55 | dataKey: 'reaction', 56 | existsWhen: condition.codeOrDisplayname, 57 | required: true 58 | }, { 59 | key: "entryRelationship", 60 | attributes: { 61 | "typeCode": "SUBJ", 62 | "inversionInd": "true" 63 | }, 64 | content: severityObservation, 65 | existsWhen: condition.keyExists('severity') 66 | } 67 | ], 68 | notImplemented: [ 69 | "Procedure Activity Procedure", 70 | "Medication Activity" 71 | ] 72 | }; 73 | 74 | exports.serviceDeliveryLocation = { 75 | key: "participantRole", 76 | attributes: { 77 | classCode: "SDLOC" 78 | }, 79 | content: [ 80 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.32"), { 81 | key: "code", 82 | attributes: leafLevel.code, 83 | dataKey: "location_type", 84 | required: true 85 | }, 86 | fieldLevel.usRealmAddress, 87 | fieldLevel.telecom, { 88 | key: "playingEntity", 89 | attributes: { 90 | classCode: "PLC" 91 | }, 92 | content: { 93 | key: "name", 94 | text: leafLevel.inputProperty("name"), 95 | }, 96 | existsWhen: condition.keyExists("name") 97 | } 98 | ] 99 | }; 100 | 101 | exports.ageObservation = { 102 | key: "observation", 103 | attributes: { 104 | classCode: "OBS", 105 | moodCode: "EVN" 106 | }, 107 | content: [ 108 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.31"), 109 | fieldLevel.templateCode("AgeObservation"), 110 | fieldLevel.statusCodeCompleted, { 111 | key: "value", 112 | attributes: { 113 | "xsi:type": "PQ", 114 | value: leafLevel.inputProperty("onset_age"), 115 | unit: leafLevel.codeOnlyFromName("2.16.840.1.113883.11.20.9.21", "onset_age_unit") 116 | }, 117 | required: true 118 | } 119 | ] 120 | }; 121 | 122 | exports.indication = { 123 | key: "observation", 124 | attributes: { 125 | classCode: "OBS", 126 | moodCode: "EVN" 127 | }, 128 | content: [ 129 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.19"), 130 | fieldLevel.id, { 131 | key: "code", 132 | attributes: leafLevel.code, 133 | dataKey: "code", 134 | required: true 135 | }, 136 | fieldLevel.statusCodeCompleted, 137 | fieldLevel.effectiveTime, { 138 | key: "value", 139 | attributes: [ 140 | leafLevel.typeCD, 141 | leafLevel.code 142 | ], 143 | dataKey: "value", 144 | existsWhen: condition.codeOrDisplayname 145 | } 146 | ], 147 | notImplemented: [ 148 | "value should handle nullFlavor=OTH and translation" 149 | ] 150 | }; 151 | 152 | exports.preconditionForSubstanceAdministration = { 153 | key: "criterion", 154 | content: [{ 155 | key: "code", 156 | attributes: { 157 | code: leafLevel.inputProperty("code"), 158 | codeSystem: "2.16.840.1.113883.5.4" 159 | }, 160 | dataKey: "code" 161 | }, { 162 | key: "value", 163 | attributes: [ 164 | leafLevel.typeCE, // TODO: spec has CD, spec example has CE 165 | leafLevel.code 166 | ], 167 | dataKey: "value", 168 | existsWhen: condition.codeOrDisplayname 169 | }], 170 | warning: [ 171 | "value type is CE is example but CD in spec", 172 | "templateId should be here according to spec but per CCD_1 is put in the parent" 173 | ] 174 | }; 175 | 176 | exports.drugVehicle = { 177 | key: "participantRole", 178 | attributes: { 179 | classCode: "MANU" 180 | }, 181 | content: [ 182 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.24"), { 183 | key: "code", 184 | attributes: { 185 | code: "412307009", 186 | displayName: "drug vehicle", 187 | codeSystem: "2.16.840.1.113883.6.96", 188 | codeSystemName: "SNOMED CT" 189 | } 190 | }, { 191 | key: "playingEntity", 192 | attributes: { 193 | classCode: "MMAT" 194 | }, 195 | content: [{ 196 | key: "code", 197 | attributes: leafLevel.code, 198 | required: true 199 | }, { 200 | key: "name", 201 | text: leafLevel.inputProperty("name") 202 | }], 203 | required: true 204 | } 205 | ] 206 | }; 207 | 208 | exports.instructions = { 209 | key: "act", 210 | attributes: { 211 | classCode: "ACT", 212 | moodCode: "INT" 213 | }, 214 | content: [ 215 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.20"), { 216 | key: "code", 217 | attributes: [ 218 | leafLevel.code 219 | ], 220 | dataKey: "code", 221 | required: true 222 | }, 223 | fieldLevel.text(leafLevel.nextReference("instruction")), 224 | fieldLevel.statusCodeCompleted 225 | ] 226 | }; 227 | -------------------------------------------------------------------------------- /lib/entryLevel/procedureEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var sharedEntryLevel = require("./sharedEntryLevel"); 9 | 10 | var key = contentModifier.key; 11 | var required = contentModifier.required; 12 | var dataKey = contentModifier.dataKey; 13 | 14 | exports.procedureActivityAct = { 15 | key: "act", 16 | attributes: { 17 | classCode: "ACT", 18 | moodCode: "INT" // not constant in the specification 19 | }, 20 | content: [ 21 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.12"), 22 | fieldLevel.uniqueId, 23 | fieldLevel.id, { 24 | key: "code", 25 | attributes: leafLevel.code, 26 | content: [{ 27 | key: "originalText", 28 | content: [{ 29 | key: "reference", 30 | attributes: { 31 | "value": leafLevel.nextReference("procedure") 32 | } 33 | }] 34 | }], 35 | dataKey: "procedure", 36 | required: true 37 | }, { 38 | key: "statusCode", 39 | attributes: { 40 | code: leafLevel.inputProperty("status") 41 | }, 42 | required: true 43 | }, 44 | fieldLevel.effectiveTime, { 45 | key: "priorityCode", 46 | attributes: leafLevel.code, 47 | dataKey: "priority" 48 | }, { 49 | key: "targetSiteCode", 50 | attributes: leafLevel.code, 51 | dataKey: "body_sites" 52 | }, 53 | [fieldLevel.performer, dataKey("performer")], { 54 | key: "participant", 55 | attributes: { 56 | typeCode: "LOC" 57 | }, 58 | content: [ 59 | [sharedEntryLevel.serviceDeliveryLocation, required] 60 | ], 61 | dataKey: "locations" 62 | } 63 | ], 64 | existsWhen: condition.propertyEquals("procedure_type", "act"), 65 | toDo: ["moodCode should be variable"], 66 | notImplemented: [ 67 | "entryRelationship:encounter", 68 | "entryRelationship:indication", 69 | "entryRelationship:medicationActivity" 70 | ] 71 | }; 72 | 73 | exports.procedureActivityProcedure = { 74 | key: "procedure", 75 | attributes: { 76 | classCode: "PROC", 77 | moodCode: "EVN" 78 | }, 79 | content: [ 80 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.14"), 81 | fieldLevel.uniqueId, 82 | fieldLevel.id, { 83 | key: "code", 84 | attributes: leafLevel.code, 85 | content: [{ 86 | key: "originalText", 87 | content: [{ 88 | key: "reference", 89 | attributes: { 90 | "value": leafLevel.nextReference("procedure") 91 | } 92 | }] 93 | }], 94 | dataKey: "procedure", 95 | required: true 96 | }, { 97 | key: "statusCode", 98 | attributes: { 99 | code: leafLevel.inputProperty("status") 100 | }, 101 | required: true 102 | }, 103 | fieldLevel.effectiveTime, { 104 | key: "priorityCode", 105 | attributes: leafLevel.code, 106 | dataKey: "priority" 107 | }, { 108 | key: "targetSiteCode", 109 | attributes: leafLevel.code, 110 | dataKey: "body_sites" 111 | }, { 112 | key: "specimen", 113 | attributes: { 114 | typeCode: "SPC" 115 | }, 116 | content: { 117 | key: "specimenRole", 118 | attributes: { 119 | classCode: "SPEC" 120 | }, 121 | content: [ 122 | fieldLevel.id, { 123 | key: "specimenPlayingEntity", 124 | content: { 125 | key: "code", 126 | attributes: leafLevel.code, 127 | dataKey: "code" 128 | }, 129 | existsWhen: condition.keyExists("code") 130 | } 131 | ], 132 | required: true 133 | }, 134 | dataKey: "specimen" 135 | }, 136 | [fieldLevel.performer, dataKey("performer")], { 137 | key: "participant", 138 | attributes: { 139 | typeCode: "LOC" 140 | }, 141 | content: [ 142 | [sharedEntryLevel.serviceDeliveryLocation, required] 143 | ], 144 | dataKey: "locations" 145 | } 146 | ], 147 | existsWhen: condition.propertyEquals("procedure_type", "procedure"), 148 | toDo: ["moodCode should be variable"], 149 | notImplemented: [ 150 | "methodCode", 151 | "participant:productInstance", 152 | "entryRelationship:encounter", 153 | "entryRelationship:instructions", 154 | "entryRelationship:indication", 155 | "entryRelationship:medicationActivity" 156 | ] 157 | }; 158 | 159 | exports.procedureActivityObservation = { 160 | key: "observation", 161 | attributes: { 162 | classCode: "OBS", 163 | moodCode: "EVN" // not constant in the specification 164 | }, 165 | content: [ 166 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.13"), 167 | fieldLevel.uniqueId, 168 | fieldLevel.id, { 169 | key: "code", 170 | attributes: leafLevel.code, 171 | content: [{ 172 | key: "originalText", 173 | content: [{ 174 | key: "reference", 175 | attributes: { 176 | "value": leafLevel.nextReference("procedure") 177 | } 178 | }] 179 | }], 180 | dataKey: "procedure", 181 | required: true 182 | }, { 183 | key: "statusCode", 184 | attributes: { 185 | code: leafLevel.inputProperty("status") 186 | }, 187 | required: true 188 | }, 189 | fieldLevel.effectiveTime, { 190 | key: "priorityCode", 191 | attributes: leafLevel.code, 192 | dataKey: "priority" 193 | }, { 194 | key: "value", 195 | attributes: { 196 | "xsi:type": "CD" 197 | } 198 | }, { 199 | key: "targetSiteCode", 200 | attributes: leafLevel.code, 201 | dataKey: "body_sites" 202 | }, 203 | [fieldLevel.performer, dataKey("performers")], { 204 | key: "participant", 205 | attributes: { 206 | typeCode: "LOC" 207 | }, 208 | content: [ 209 | [sharedEntryLevel.serviceDeliveryLocation, required] 210 | ], 211 | dataKey: "locations" 212 | } 213 | ], 214 | existsWhen: condition.propertyEquals("procedure_type", "observation"), 215 | toDo: ["moodCode should be variable"], 216 | notImplemented: [ 217 | "entryRelationship:encounter", 218 | "entryRelationship:instructions", 219 | "entryRelationship:indication", 220 | "entryRelationship:medicationActivity" 221 | ] 222 | }; 223 | -------------------------------------------------------------------------------- /test/util/xpathutil.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var libxmljs = require('libxmljs'); 4 | 5 | var ns = { 6 | "h": "urn:hl7-org:v3", 7 | "xsi": "http://www.w3.org/2001/XMLSchema-instance" 8 | }; 9 | 10 | var templateIdPath = function (templateId, prefix, postfix) { 11 | var p = '//h:templateId[@root="' + templateId + '"]/..'; 12 | if (prefix) { 13 | p = prefix + p; 14 | } 15 | if (postfix) { 16 | p = p + postfix; 17 | } 18 | return p; 19 | }; 20 | 21 | var templateIdPathFromSpec = function (spec, prefix, postfix) { 22 | if (Array.isArray(spec)) { 23 | var paths = spec.map(function (templateId) { 24 | return templateIdPath(templateId, prefix, postfix); 25 | }); 26 | return paths.join(' | '); 27 | } else { 28 | return templateIdPath(spec, prefix, postfix); 29 | } 30 | }; 31 | 32 | var pathConstructor = { 33 | 'rootTemplate': function (value) { 34 | return templateIdPathFromSpec(value); 35 | }, 36 | 'localTemplateParent': function (value) { 37 | return templateIdPathFromSpec(value, '.', '/..'); 38 | }, 39 | 'localTemplate': function (value) { 40 | return templateIdPathFromSpec(value, '.'); 41 | }, 42 | "normal": function (value) { 43 | return value; 44 | } 45 | }; 46 | 47 | // The order of actions in this list is important. There is a bug in 48 | // libxmljs: if you access a node and later remove a parent or itself 49 | // you get segmentation faults. See https://github.com/polotek/libxmljs/pull/163 50 | var actions = [{ 51 | key: "removeNode", 52 | implementation: function (node) { 53 | node.remove(); 54 | } 55 | }, { 56 | key: "removeAttribute", 57 | implementation: function (node, attr) { 58 | var attrNode = node.getAttribute(attr); 59 | if (attrNode) { 60 | attrNode.remove(); 61 | } 62 | } 63 | }, { 64 | key: "flatten", 65 | implementation: function (node, tid) { 66 | var childrenPath = pathConstructor.localTemplate(tid); 67 | var newChildren = node.find(childrenPath, ns).map(function (v) { 68 | var newChild = v.clone(); 69 | v.remove(); 70 | return newChild; 71 | }); 72 | var p = node.parent(); 73 | newChildren.forEach(function (newChild) { 74 | p.addChild(newChild); 75 | }); 76 | } 77 | }, { 78 | key: "addAttribute", 79 | implementation: function (node, params) { 80 | node.attr(params); 81 | } 82 | }, { 83 | key: "addAttributeWhenEmpty", 84 | implementation: function (node, params) { 85 | var allEmpty = Object.keys(params).every(function (param) { 86 | var attrNode = node.attr(param); 87 | return !attrNode; 88 | }); 89 | if (allEmpty) { 90 | node.attr(params); 91 | } 92 | } 93 | }, { 94 | key: "normalizeTelNumber", 95 | implementation: function (node) { 96 | var attrNode = node.getAttribute('value'); 97 | if (attrNode) { 98 | var value = attrNode.toString(); 99 | if (value.substring(0, 4) !== 'tel:') { 100 | var newValue = 'tel:' + value; 101 | attrNode.value(newValue); 102 | } 103 | } 104 | } 105 | }, { 106 | key: "removeTimezone", 107 | implementation: function (node) { 108 | var attrNode = node.getAttribute('value'); 109 | if (attrNode) { 110 | var t = attrNode.toString(); 111 | var newT = t.slice(0, 8); // Ignore time for now 112 | attrNode.value(newT); 113 | } 114 | } 115 | }, { 116 | key: "removeZeros", 117 | implementation: function (node) { 118 | var attrNode = node.getAttribute('value'); 119 | if (attrNode) { 120 | var v = attrNode.toString(); 121 | var n = v.length; 122 | var index = v.indexOf('.0'); 123 | if ((index >= 0) && ((index + 2) === n)) { 124 | v = v.slice(0, index); 125 | attrNode.value(v); 126 | } else if ((v.charAt(n - 1) === '0') && (v.indexOf('.') >= 0)) { 127 | v = v.slice(0, n - 1); 128 | attrNode.value(v); 129 | } else if (v === '00') { 130 | attrNode.value('0'); 131 | } 132 | } 133 | } 134 | }, { 135 | key: "replaceText", 136 | implementation: function (node, map) { 137 | var text = node.text(); 138 | var replacementText = map[text]; 139 | if (replacementText) { 140 | node.text(replacementText); 141 | } 142 | } 143 | }, { 144 | key: "normalize", 145 | implementation: function (node, params) { 146 | var attrNode = node.getAttribute(params.attr); 147 | var value = attrNode.value(); 148 | var replacementInfo = params.map[value]; 149 | if (replacementInfo) { 150 | var replacementValue; 151 | if (typeof replacementInfo === 'object') { 152 | var srcAttrValue = node.getAttribute(params.srcAttr).value(); 153 | if (srcAttrValue === replacementInfo.src) { 154 | replacementValue = replacementInfo.value; 155 | } 156 | } else { 157 | replacementValue = replacementInfo; 158 | } 159 | if (replacementValue) { 160 | attrNode.value(replacementValue); 161 | } 162 | } 163 | } 164 | }]; 165 | 166 | var doModifications = (function () { 167 | var sortModications = function (modifications) { 168 | var grouped = modifications.reduce(function (r, mod) { 169 | var action = mod.action; 170 | var group = r[action]; 171 | if (!group) { 172 | group = r[action] = []; 173 | } 174 | group.push(mod); 175 | return r; 176 | }, {}); 177 | return actions.reduce(function (r, action) { 178 | var key = action.key; 179 | var groupMods = grouped[key]; 180 | if (groupMods) { 181 | r = r.concat(groupMods); 182 | } 183 | return r; 184 | }, []); 185 | }; 186 | 187 | var actionMap = actions.reduce(function (r, action) { 188 | r[action.key] = action.implementation; 189 | return r; 190 | }, {}); 191 | 192 | return function doModifications(xmlDoc, modifications) { 193 | var sorted = sortModications(modifications); 194 | sorted.forEach(function (modification) { 195 | var pathType = modification.type || "normal"; 196 | var path = pathConstructor[pathType](modification.xpath); 197 | var nodes = xmlDoc.find(path, ns); 198 | nodes.forEach(function (node) { 199 | var actionKey = modification.action; 200 | actionMap[actionKey](node, modification.params); 201 | }); 202 | }); 203 | }; 204 | })(); 205 | 206 | exports.modifyXML = function (xml, modifications) { 207 | var xmlDoc = libxmljs.parseXml(xml, { 208 | preserveWhitespace: true 209 | }); 210 | doModifications(xmlDoc, modifications); 211 | var result = xmlDoc.toString(); 212 | return result; 213 | }; 214 | -------------------------------------------------------------------------------- /lib/fieldLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var bbm = require("@amida-tech/blue-button-meta"); 4 | var uuid = require('uuid'); 5 | 6 | var condition = require("./condition"); 7 | var leafLevel = require("./leafLevel"); 8 | var translate = require("./translate"); 9 | var contentModifier = require("./contentModifier"); 10 | 11 | var templateCodes = bbm.CCDA.sections_entries_codes.codes; 12 | 13 | var key = contentModifier.key; 14 | var required = contentModifier.required; 15 | 16 | var moment = require('moment'); 17 | 18 | exports.templateId = function (id) { 19 | return { 20 | key: "templateId", 21 | attributes: { 22 | "root": id 23 | } 24 | }; 25 | }; 26 | 27 | exports.templateCode = function (name) { 28 | var raw = templateCodes[name]; 29 | var result = { 30 | key: "code", 31 | attributes: { 32 | code: raw.code, 33 | displayName: raw.name, 34 | codeSystem: raw.code_system, 35 | codeSystemName: raw.code_system_name 36 | } 37 | }; 38 | return result; 39 | }; 40 | 41 | exports.templateTitle = function (name) { 42 | var raw = templateCodes[name]; 43 | var result = { 44 | key: "title", 45 | text: raw.name, 46 | }; 47 | return result; 48 | }; 49 | 50 | var id = exports.id = { 51 | key: "id", 52 | attributes: { 53 | root: leafLevel.inputProperty("identifier"), 54 | extension: leafLevel.inputProperty("extension") 55 | }, 56 | dataKey: 'identifiers', 57 | existsWhen: condition.keyExists('identifier'), 58 | required: true 59 | }; 60 | 61 | exports.uniqueId = { 62 | key: "id", 63 | attributes: { 64 | root: function (input, context) { 65 | return context.rootId; 66 | }, 67 | extension: function () { 68 | return uuid.v4(); 69 | } 70 | }, 71 | existsWhen: function (input, context) { 72 | return context.rootId; 73 | } 74 | }; 75 | 76 | exports.statusCodeCompleted = { 77 | key: "statusCode", 78 | attributes: { 79 | code: 'completed' 80 | } 81 | }; 82 | 83 | exports.statusCodeActive = { 84 | key: "statusCode", 85 | attributes: { 86 | code: 'active' 87 | } 88 | }; 89 | 90 | exports.statusCodeNew = { 91 | key: "statusCode", 92 | attributes: { 93 | code: 'new' 94 | } 95 | }; 96 | 97 | var effectiveTimeNow = exports.effectiveTimeNow = { 98 | key: "effectiveTime", 99 | attributes: { 100 | "value": moment().format("YYYYMMDDHHMMSSZZ"), 101 | } 102 | }; 103 | 104 | var timeNow = exports.timeNow = { 105 | key: "time", 106 | attributes: { 107 | "value": moment().format("YYYYMMDDHHMMSSZZ"), 108 | } 109 | }; 110 | 111 | var effectiveTime = exports.effectiveTime = { 112 | key: "effectiveTime", 113 | attributes: { 114 | "value": leafLevel.time, 115 | }, 116 | attributeKey: 'point', 117 | content: [{ 118 | key: "low", 119 | attributes: { 120 | "value": leafLevel.time 121 | }, 122 | dataKey: 'low', 123 | }, { 124 | key: "high", 125 | attributes: { 126 | "value": leafLevel.time 127 | }, 128 | dataKey: 'high', 129 | }, { 130 | key: "center", 131 | attributes: { 132 | "value": leafLevel.time 133 | }, 134 | dataKey: 'center', 135 | }], 136 | dataKey: 'date_time', 137 | existsWhen: condition.eitherKeyExists('point', 'low', 'high', 'center') 138 | }; 139 | 140 | exports.text = function (referenceMethod) { 141 | return { 142 | key: "text", 143 | text: leafLevel.inputProperty("free_text"), 144 | content: { 145 | key: "reference", 146 | attributes: { 147 | "value": referenceMethod 148 | }, 149 | } 150 | }; 151 | }; 152 | 153 | exports.nullFlavor = function (name) { 154 | return { 155 | key: name, 156 | attributes: { 157 | nullFlavor: "UNK" 158 | } 159 | }; 160 | }; 161 | 162 | var usRealmAddress = exports.usRealmAddress = { 163 | key: "addr", 164 | attributes: { 165 | use: leafLevel.use("use") 166 | }, 167 | content: [{ 168 | key: "country", 169 | text: leafLevel.inputProperty("country") 170 | }, { 171 | key: "state", 172 | text: leafLevel.inputProperty("state") 173 | }, { 174 | key: "city", 175 | text: leafLevel.inputProperty("city") 176 | }, { 177 | key: "postalCode", 178 | text: leafLevel.inputProperty("zip") 179 | }, { 180 | key: "streetAddressLine", 181 | text: leafLevel.input, 182 | dataKey: "street_lines" 183 | }], 184 | dataKey: "address" 185 | }; 186 | 187 | var usRealmName = exports.usRealmName = { 188 | key: "name", 189 | content: [{ 190 | key: "family", 191 | text: leafLevel.inputProperty("family") 192 | }, { 193 | key: "given", 194 | text: leafLevel.input, 195 | dataKey: "given" 196 | }, { 197 | key: "prefix", 198 | text: leafLevel.inputProperty("prefix") 199 | }, { 200 | key: "suffix", 201 | text: leafLevel.inputProperty("suffix") 202 | }], 203 | dataKey: "name", 204 | dataTransform: translate.name 205 | }; 206 | 207 | var telecom = exports.telecom = { 208 | key: "telecom", 209 | attributes: { 210 | value: leafLevel.inputProperty("value"), 211 | use: leafLevel.inputProperty("use") 212 | }, 213 | dataTransform: translate.telecom 214 | }; 215 | 216 | var representedOrganization = { 217 | key: "representedOrganization", 218 | content: [ 219 | id, { 220 | key: "id", 221 | attributes: { 222 | extension: leafLevel.inputProperty("extension"), 223 | root: leafLevel.inputProperty("root") 224 | }, 225 | dataKey: "identity" 226 | }, { 227 | key: "name", 228 | text: leafLevel.input, 229 | dataKey: "name" 230 | }, 231 | usRealmAddress, 232 | telecom, { 233 | key: "telecom", 234 | attributes: [{ 235 | use: "WP", 236 | value: function (input) { 237 | return input.value.number; 238 | } 239 | }], 240 | existsWhen: condition.keyExists("value"), 241 | dataKey: "phone" 242 | } 243 | ], 244 | dataKey: "organization" 245 | }; 246 | 247 | var assignedEntity = exports.assignedEntity = { 248 | key: "assignedEntity", 249 | content: [id, { 250 | key: "code", 251 | attributes: leafLevel.code, 252 | dataKey: "code" 253 | }, 254 | 255 | usRealmAddress, 256 | telecom, { 257 | key: "assignedPerson", 258 | content: usRealmName, 259 | existsWhen: condition.keyExists("name") 260 | }, 261 | representedOrganization 262 | ], 263 | existsWhen: condition.eitherKeyExists("address", "identifiers", "organization", "name") 264 | }; 265 | 266 | exports.author = { 267 | key: "author", 268 | content: [{ 269 | key: "time", 270 | attributes: { 271 | nullFlavor: "UNK" 272 | }, 273 | }, 274 | [effectiveTime, required, key("time")], { 275 | key: "assignedAuthor", 276 | content: [ 277 | id, { 278 | key: "assignedPerson", 279 | content: usRealmName 280 | }, 281 | representedOrganization 282 | ] 283 | } 284 | ], 285 | dataKey: "author" 286 | }; 287 | 288 | exports.performer = { 289 | key: "performer", 290 | content: [ 291 | [assignedEntity, required] 292 | ], 293 | dataKey: "performer" 294 | }; 295 | -------------------------------------------------------------------------------- /lib/entryLevel/medicationEntryLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('../fieldLevel'); 4 | var leafLevel = require('../leafLevel'); 5 | var condition = require("../condition"); 6 | var contentModifier = require("../contentModifier"); 7 | 8 | var sharedEntryLevel = require("./sharedEntryLevel"); 9 | 10 | var key = contentModifier.key; 11 | var required = contentModifier.required; 12 | var dataKey = contentModifier.dataKey; 13 | 14 | var medicationInformation = { 15 | key: "manufacturedProduct", 16 | attributes: { 17 | classCode: "MANU" 18 | }, 19 | content: [ 20 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.23"), 21 | fieldLevel.id, { 22 | key: "manufacturedMaterial", 23 | content: [{ 24 | key: "code", 25 | attributes: leafLevel.code, 26 | content: [{ 27 | key: "originalText", 28 | text: leafLevel.inputProperty("unencoded_name"), 29 | content: [{ 30 | key: "reference", 31 | attributes: { 32 | "value": leafLevel.nextReference("medinfo") 33 | } 34 | }] 35 | }, { 36 | key: "translation", 37 | attributes: leafLevel.code, 38 | dataKey: "translations" 39 | }] 40 | }], 41 | dataKey: "product", 42 | required: true 43 | }, { 44 | key: "manufacturerOrganization", 45 | content: { 46 | key: "name", 47 | text: leafLevel.input, 48 | }, 49 | dataKey: "manufacturer" 50 | } 51 | ], 52 | dataTransform: function (input) { 53 | if (input.product) { 54 | input.product.unencoded_name = input.unencoded_name; 55 | } 56 | return input; 57 | } 58 | }; 59 | 60 | var medicationSupplyOrder = { 61 | key: "supply", 62 | attributes: { 63 | classCode: "SPLY", 64 | moodCode: "INT" 65 | }, 66 | content: [ 67 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.17"), 68 | fieldLevel.id, 69 | fieldLevel.statusCodeCompleted, 70 | fieldLevel.effectiveTime, { 71 | key: "repeatNumber", 72 | attributes: { 73 | value: leafLevel.input 74 | }, 75 | dataKey: "repeatNumber" 76 | }, { 77 | key: "quantity", 78 | attributes: { 79 | value: leafLevel.input 80 | }, 81 | dataKey: "quantity" 82 | }, { 83 | key: "product", 84 | content: medicationInformation, 85 | dataKey: "product" 86 | }, 87 | fieldLevel.author, { 88 | key: "entryRelationship", 89 | attributes: { 90 | typeCode: "SUBJ", 91 | inversionInd: "true" 92 | }, 93 | content: [ 94 | [sharedEntryLevel.instructions, required] 95 | ], 96 | dataKey: "instructions" 97 | } 98 | ], 99 | toDo: "statusCode needs to allow values other than completed", 100 | notImplemented: [ 101 | "product:immunizationMedicationInformation" 102 | ] 103 | }; 104 | 105 | var medicationDispense = { 106 | key: "supply", 107 | attributes: { 108 | classCode: "SPLY", 109 | moodCode: "EVN" 110 | }, 111 | content: [ 112 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.18"), 113 | fieldLevel.id, 114 | fieldLevel.statusCodeCompleted, 115 | fieldLevel.effectiveTime, { 116 | key: "product", 117 | content: medicationInformation, 118 | dataKey: "product" 119 | }, 120 | fieldLevel.performer 121 | ], 122 | toDo: "statusCode needs to allow different values than completed", 123 | notImplemented: [ 124 | "repeatNumber", 125 | "quantity", 126 | "product:ImmunizationMedicationInformation", 127 | "entryRelationship:medicationSupplyOrder", 128 | ] 129 | }; 130 | 131 | exports.medicationActivity = { 132 | key: "substanceAdministration", 133 | attributes: { 134 | classCode: "SBADM", 135 | moodCode: function (input) { 136 | var status = input.status; 137 | if (status) { 138 | if (status === 'Prescribed') { 139 | return 'INT'; 140 | } 141 | if (status === 'Completed') { 142 | return 'EVN'; 143 | } 144 | } 145 | return null; 146 | } 147 | }, 148 | content: [ 149 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.16"), 150 | fieldLevel.uniqueId, 151 | fieldLevel.id, { 152 | key: "text", 153 | text: leafLevel.input, 154 | dataKey: "sig" 155 | }, 156 | fieldLevel.statusCodeCompleted, [fieldLevel.effectiveTime, required], { 157 | key: "effectiveTime", 158 | attributes: { 159 | "xsi:type": "PIVL_TS", 160 | "institutionSpecified": "true", 161 | "operator": "A" 162 | }, 163 | content: { 164 | key: "period", 165 | attributes: { 166 | value: leafLevel.inputProperty("value"), 167 | unit: leafLevel.inputProperty("unit") 168 | }, 169 | }, 170 | dataKey: "administration.interval.period", 171 | }, { 172 | key: "routeCode", 173 | attributes: leafLevel.code, 174 | dataKey: "administration.route" 175 | }, { 176 | key: "doseQuantity", 177 | attributes: { 178 | value: leafLevel.inputProperty("value"), 179 | unit: leafLevel.inputProperty("unit") 180 | }, 181 | dataKey: "administration.dose" 182 | }, { 183 | key: "rateQuantity", 184 | attributes: { 185 | value: leafLevel.inputProperty("value"), 186 | unit: leafLevel.inputProperty("unit") 187 | }, 188 | dataKey: "administration.rate" 189 | }, { 190 | key: "administrationUnitCode", 191 | attributes: leafLevel.code, 192 | dataKey: "administration.form" 193 | }, { 194 | key: "consumable", 195 | content: medicationInformation, 196 | dataKey: "product" 197 | }, 198 | fieldLevel.performer, { 199 | key: "participant", 200 | attributes: { 201 | typeCode: "CSM" 202 | }, 203 | content: [ 204 | [sharedEntryLevel.drugVehicle, required] 205 | ], 206 | dataKey: "drug_vehicle" 207 | }, { 208 | key: "entryRelationship", 209 | attributes: { 210 | typeCode: "RSON" 211 | }, 212 | content: [ 213 | [sharedEntryLevel.indication, required] 214 | ], 215 | dataKey: "indication" 216 | }, { 217 | key: "entryRelationship", 218 | attributes: { 219 | typeCode: "REFR" 220 | }, 221 | content: [ 222 | [medicationSupplyOrder, required] 223 | ], 224 | dataKey: "supply" 225 | }, { 226 | key: "entryRelationship", 227 | attributes: { 228 | typeCode: "REFR" 229 | }, 230 | content: [ 231 | [medicationDispense, required] 232 | ], 233 | dataKey: "dispense" 234 | }, { 235 | key: "precondition", 236 | attributes: { 237 | typeCode: "PRCN" 238 | }, 239 | content: [ 240 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.25"), [sharedEntryLevel.preconditionForSubstanceAdministration, required] 241 | ], 242 | dataKey: "precondition", 243 | warning: "templateId needs to be in preconditionForSubstanceAdministration but CCD_1.xml contradicts" 244 | } 245 | ], 246 | notImplemented: [ 247 | "code", 248 | "text:reference", 249 | "repeatNumber", 250 | "approachSiteCode", 251 | "maxDoseQuantity", 252 | "entryRelationship:instructions", 253 | "reactionObservation" 254 | ] 255 | }; 256 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/jest_rs", 15 | 16 | // Automatically clear mock calls, instances and results before every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state before every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state and implementation before every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | testMatch: [ 150 | "**/test/**/*.[jt]s?(x)", 151 | "**/?(*.)+(spec|test).[tj]s?(x)" 152 | ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | testPathIgnorePatterns: [ 156 | "/node_modules/", 157 | "/test/(util|xmlmods)", 158 | ], 159 | 160 | // The regexp pattern or array of patterns that Jest uses to detect test files 161 | // testRegex: [], 162 | 163 | // This option allows the use of a custom results processor 164 | // testResultsProcessor: undefined, 165 | 166 | // This option allows use of a custom test runner 167 | // testRunner: "jest-circus/runner", 168 | 169 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 170 | // testURL: "http://localhost", 171 | 172 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 173 | // timers: "real", 174 | 175 | // A map from regular expressions to paths to transformers 176 | // transform: undefined, 177 | 178 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 179 | // transformIgnorePatterns: [ 180 | // "/node_modules/", 181 | // "\\.pnp\\.[^\\/]+$" 182 | // ], 183 | 184 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 185 | // unmockedModulePathPatterns: undefined, 186 | 187 | // Indicates whether each individual test should be reported during the run 188 | // verbose: undefined, 189 | 190 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 191 | // watchPathIgnorePatterns: [], 192 | 193 | // Whether to use watchman for file crawling 194 | // watchman: true, 195 | }; 196 | -------------------------------------------------------------------------------- /lib/documentLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var headerLevel = require('./headerLevel'); 4 | var fieldLevel = require('./fieldLevel'); 5 | var leafLevel = require('./leafLevel'); 6 | var sectionLevel = require('./sectionLevel'); 7 | var contentModifier = require("./contentModifier"); 8 | var condition = require("./condition"); 9 | 10 | var required = contentModifier.required; 11 | var dataKey = contentModifier.dataKey; 12 | 13 | exports.ccd = { 14 | key: "ClinicalDocument", 15 | attributes: { 16 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", 17 | "xmlns": "urn:hl7-org:v3", 18 | "xmlns:cda": "urn:hl7-org:v3", 19 | "xmlns:sdtc": "urn:hl7-org:sdtc" 20 | }, 21 | content: [{ 22 | key: "realmCode", 23 | attributes: { 24 | code: "US" 25 | } 26 | }, { 27 | key: "typeId", 28 | attributes: { 29 | root: "2.16.840.1.113883.1.3", 30 | extension: "POCD_HD000040" 31 | } 32 | }, 33 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.1.1"), 34 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.1.2"), [fieldLevel.id, dataKey("meta.identifiers")], { 35 | key: "code", 36 | attributes: { 37 | codeSystem: "2.16.840.1.113883.6.1", 38 | codeSystemName: "LOINC", 39 | code: "34133-9", 40 | displayName: "Summarization of Episode Note" 41 | } 42 | }, { 43 | key: "title", 44 | text: leafLevel.inputProperty("title"), 45 | dataKey: "meta.ccda_header" 46 | }, 47 | [fieldLevel.effectiveTimeNow, required], { 48 | key: "confidentialityCode", 49 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.25"), 50 | dataKey: "meta.confidentiality" 51 | }, { 52 | key: "languageCode", 53 | attributes: { 54 | code: "en-US" 55 | } 56 | }, { 57 | key: "setId", 58 | attributes: { 59 | root: leafLevel.inputProperty("identifier"), 60 | extension: leafLevel.inputProperty("extension") 61 | }, 62 | dataKey: 'meta.set_id', 63 | existsWhen: condition.keyExists('identifier') 64 | }, { 65 | key: "versionNumber", 66 | attributes: { 67 | value: "1" 68 | } 69 | }, 70 | headerLevel.recordTarget, 71 | headerLevel.headerAuthor, 72 | headerLevel.headerInformant, 73 | headerLevel.headerCustodian, 74 | headerLevel.providers, { 75 | key: "component", 76 | content: { 77 | key: "structuredBody", 78 | content: [ 79 | [sectionLevel.allergiesSectionEntriesRequired, required], 80 | [sectionLevel.medicationsSectionEntriesRequired, required], 81 | [sectionLevel.problemsSectionEntriesRequired, required], 82 | [sectionLevel.proceduresSectionEntriesRequired, required], 83 | [sectionLevel.resultsSectionEntriesRequired, required], 84 | sectionLevel.encountersSectionEntriesOptional, 85 | sectionLevel.immunizationsSectionEntriesOptional, 86 | sectionLevel.payersSection, 87 | sectionLevel.planOfCareSection, 88 | sectionLevel.socialHistorySection, 89 | sectionLevel.vitalSignsSectionEntriesOptional 90 | ], 91 | notImplemented: [ 92 | "advanceDirectivesSectionEntriesOptional", 93 | "familyHistorySection", 94 | "functionalStatusSection", 95 | "medicalEquipmentSection", 96 | ] 97 | }, 98 | dataKey: 'data' 99 | } 100 | ] 101 | }; 102 | 103 | var sectionLevel2 = require('./sectionLevel2'); 104 | 105 | exports.ccd2 = function (html_renderer) { 106 | var ccd_template = { 107 | key: "ClinicalDocument", 108 | attributes: { 109 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", 110 | "xmlns": "urn:hl7-org:v3", 111 | "xmlns:cda": "urn:hl7-org:v3", 112 | "xmlns:sdtc": "urn:hl7-org:sdtc" 113 | }, 114 | content: [{ 115 | key: "realmCode", 116 | attributes: { 117 | code: "US" 118 | } 119 | }, { 120 | key: "typeId", 121 | attributes: { 122 | root: "2.16.840.1.113883.1.3", 123 | extension: "POCD_HD000040" 124 | } 125 | }, 126 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.1.1"), 127 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.1.2"), [fieldLevel.id, dataKey("meta.identifiers")], { 128 | key: "code", 129 | attributes: { 130 | codeSystem: "2.16.840.1.113883.6.1", 131 | codeSystemName: "LOINC", 132 | code: "34133-9", 133 | displayName: "Summarization of Episode Note" 134 | } 135 | }, { 136 | key: "title", 137 | text: leafLevel.inputProperty("title"), 138 | dataKey: "meta.ccda_header" 139 | }, 140 | [fieldLevel.effectiveTimeNow, required], { 141 | key: "confidentialityCode", 142 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.25"), 143 | dataKey: "meta.confidentiality" 144 | }, { 145 | key: "languageCode", 146 | attributes: { 147 | code: "en-US" 148 | } 149 | }, { 150 | key: "setId", 151 | attributes: { 152 | root: leafLevel.inputProperty("identifier"), 153 | extension: leafLevel.inputProperty("extension") 154 | }, 155 | dataKey: 'meta.set_id', 156 | existsWhen: condition.keyExists('identifier') 157 | }, { 158 | key: "versionNumber", 159 | attributes: { 160 | value: "1" 161 | } 162 | }, 163 | headerLevel.recordTarget, 164 | headerLevel.headerAuthor, 165 | headerLevel.headerInformant, 166 | headerLevel.headerCustodian, 167 | headerLevel.providers, { 168 | key: "component", 169 | content: { 170 | key: "structuredBody", 171 | content: [ 172 | [sectionLevel2.allergiesSectionEntriesRequired(html_renderer.allergiesSectionEntriesRequiredHtmlHeader, html_renderer.allergiesSectionEntriesRequiredHtmlHeaderNA), required], 173 | [sectionLevel2.medicationsSectionEntriesRequired(html_renderer.medicationsSectionEntriesRequiredHtmlHeader, html_renderer.medicationsSectionEntriesRequiredHtmlHeaderNA), required], 174 | [sectionLevel2.problemsSectionEntriesRequired(html_renderer.problemsSectionEntriesRequiredHtmlHeader, html_renderer.problemsSectionEntriesRequiredHtmlHeaderNA), required], 175 | [sectionLevel2.proceduresSectionEntriesRequired(html_renderer.proceduresSectionEntriesRequiredHtmlHeader, html_renderer.proceduresSectionEntriesRequiredHtmlHeaderNA), required], 176 | [sectionLevel2.resultsSectionEntriesRequired(html_renderer.resultsSectionEntriesRequiredHtmlHeader, html_renderer.resultsSectionEntriesRequiredHtmlHeaderNA), required], 177 | sectionLevel2.encountersSectionEntriesOptional(html_renderer.encountersSectionEntriesOptionalHtmlHeader, html_renderer.encountersSectionEntriesOptionalHtmlHeaderNA), 178 | sectionLevel2.immunizationsSectionEntriesOptional(html_renderer.immunizationsSectionEntriesOptionalHtmlHeader, html_renderer.immunizationsSectionEntriesOptionalHtmlHeaderNA), 179 | sectionLevel2.payersSection(html_renderer.payersSectionHtmlHeader, html_renderer.payersSectionHtmlHeaderNA), 180 | sectionLevel2.planOfCareSection(html_renderer.planOfCareSectionHtmlHeader, html_renderer.planOfCareSectionHtmlHeaderNA), 181 | sectionLevel2.socialHistorySection(html_renderer.socialHistorySectionHtmlHeader, html_renderer.socialHistorySectionHtmlHeaderNA), 182 | sectionLevel2.vitalSignsSectionEntriesOptional(html_renderer.vitalSignsSectionEntriesOptionalHtmlHeader, html_renderer.vitalSignsSectionEntriesOptionalHtmlHeaderNA) 183 | ], 184 | notImplemented: [ 185 | "advanceDirectivesSectionEntriesOptional", 186 | "familyHistorySection", 187 | "functionalStatusSection", 188 | "medicalEquipmentSection", 189 | ] 190 | }, 191 | dataKey: 'data' 192 | } 193 | ] 194 | }; 195 | return ccd_template; 196 | }; 197 | -------------------------------------------------------------------------------- /lib/headerLevel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fieldLevel = require('./fieldLevel'); 4 | var leafLevel = require('./leafLevel'); 5 | var condition = require('./condition'); 6 | var contentModifier = require("./contentModifier"); 7 | 8 | var key = contentModifier.key; 9 | var required = contentModifier.required; 10 | var dataKey = contentModifier.dataKey; 11 | 12 | var patientName = Object.create(fieldLevel.usRealmName); 13 | patientName.attributes = { 14 | use: "L" 15 | }; 16 | 17 | var patient = exports.patient = { 18 | key: "patient", 19 | content: [ 20 | patientName, { 21 | key: "administrativeGenderCode", 22 | attributes: { 23 | code: function (input) { 24 | return input.code; 25 | }, 26 | codeSystem: "2.16.840.1.113883.5.1", 27 | codeSystemName: "HL7 AdministrativeGender", 28 | displayName: function (input) { 29 | return input.name; 30 | } 31 | }, 32 | dataKey: "gender" 33 | }, 34 | [fieldLevel.effectiveTime, key("birthTime"), dataKey("dob")], { 35 | key: "maritalStatusCode", 36 | attributes: { 37 | code: function (input) { 38 | return input.code; 39 | }, 40 | displayName: function (input) { 41 | return input.name; 42 | }, 43 | codeSystem: "2.16.840.1.113883.5.2", 44 | codeSystemName: "HL7 Marital Status" 45 | }, 46 | dataKey: "marital_status" 47 | }, { 48 | key: "religiousAffiliationCode", 49 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.1076"), 50 | dataKey: "religion" 51 | }, { 52 | key: "ethnicGroupCode", 53 | attributes: leafLevel.codeFromName("2.16.840.1.113883.6.238"), 54 | dataKey: "ethnicity" 55 | }, { 56 | key: "raceCode", 57 | attributes: leafLevel.codeFromName("2.16.840.1.113883.6.238"), 58 | dataKey: "race" 59 | }, { 60 | key: "guardian", 61 | content: [{ 62 | key: "code", 63 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.111"), 64 | dataKey: "relation" 65 | }, 66 | [fieldLevel.usRealmAddress, dataKey("addresses")], 67 | fieldLevel.telecom, { 68 | key: "guardianPerson", 69 | content: { 70 | key: "name", 71 | content: [{ 72 | key: "given", 73 | text: leafLevel.inputProperty("first") 74 | }, { 75 | key: "family", 76 | text: leafLevel.inputProperty("last") 77 | }], 78 | dataKey: "names" 79 | } 80 | } 81 | ], 82 | dataKey: "guardians" 83 | }, { 84 | key: "birthplace", 85 | content: { 86 | key: "place", 87 | content: [ 88 | [fieldLevel.usRealmAddress, dataKey("birthplace")] 89 | ] 90 | }, 91 | existsWhen: condition.keyExists("birthplace") 92 | }, { 93 | key: "languageCommunication", 94 | content: [{ 95 | key: "languageCode", 96 | attributes: { 97 | code: function (input) { 98 | return input.code; 99 | } 100 | }, 101 | dataKey: "language" 102 | }, { 103 | key: "modeCode", 104 | attributes: leafLevel.codeFromName("2.16.840.1.113883.5.60"), 105 | dataKey: "mode" 106 | }, { 107 | key: "proficiencyLevelCode", 108 | attributes: { 109 | code: function (input) { 110 | return input.code; 111 | }, 112 | displayName: function (input) { 113 | return input.name; 114 | }, 115 | codeSystem: "2.16.840.1.113883.5.61", 116 | codeSystemName: "LanguageAbilityProficiency" 117 | }, 118 | dataKey: "proficiency" 119 | }, { 120 | key: "preferenceInd", 121 | attributes: { 122 | value: function (input) { 123 | return input.toString(); 124 | } 125 | }, 126 | dataKey: "preferred" 127 | }], 128 | dataKey: "languages" 129 | } 130 | ] 131 | }; 132 | 133 | var provider = exports.provider = { 134 | key: "performer", 135 | attributes: { 136 | typeCode: "PRF" 137 | }, 138 | content: [ 139 | [fieldLevel.effectiveTime, key("time"), dataKey("date_time")], { 140 | key: "assignedEntity", 141 | content: [{ 142 | key: "id", 143 | attributes: { 144 | extension: leafLevel.inputProperty("extension"), 145 | root: leafLevel.inputProperty("root") 146 | }, 147 | dataKey: "identity" 148 | }, { 149 | key: "code", 150 | attributes: leafLevel.code, 151 | dataKey: "type" 152 | }, 153 | 154 | { 155 | key: "telecom", 156 | attributes: [{ 157 | use: "WP", 158 | value: function (input) { 159 | return input.value.number; 160 | } 161 | }], 162 | dataKey: "phone" 163 | }, { 164 | key: "assignedPerson", 165 | content: [{ 166 | key: "name", 167 | content: [{ 168 | key: "given", 169 | text: leafLevel.inputProperty("first") 170 | }, { 171 | key: "family", 172 | text: leafLevel.inputProperty("last") 173 | }], 174 | dataKey: "name" 175 | }] 176 | } 177 | ] 178 | } 179 | ], 180 | dataKey: "providers" 181 | }; 182 | 183 | var attributed_provider = exports.attributed_provider = { 184 | key: "providerOrganization", 185 | content: [{ 186 | key: "id", 187 | attributes: { 188 | extension: leafLevel.inputProperty("extension"), 189 | root: leafLevel.inputProperty("root") 190 | }, 191 | dataKey: "attributed_provider.identity" 192 | }, { 193 | key: "name", 194 | text: leafLevel.inputProperty("full"), 195 | dataKey: "attributed_provider.name" 196 | }, { 197 | key: "telecom", 198 | attributes: [{ 199 | use: "WP", 200 | value: function (input) { 201 | return input.value.number; 202 | } 203 | }], 204 | dataKey: "attributed_provider.phone" 205 | }], 206 | dataKey: "meta" 207 | }; 208 | 209 | var recordTarget = exports.recordTarget = { 210 | key: "recordTarget", 211 | content: { 212 | key: "patientRole", 213 | content: [ 214 | fieldLevel.id, [fieldLevel.usRealmAddress, dataKey("addresses")], 215 | fieldLevel.telecom, 216 | patient, 217 | attributed_provider 218 | ] 219 | }, 220 | dataKey: "data.demographics" 221 | }; 222 | 223 | var headerAuthor = exports.headerAuthor = { 224 | key: "author", 225 | content: [ 226 | [fieldLevel.timeNow, required], { 227 | key: "assignedAuthor", 228 | //attributes: {id:} 229 | content: [{ 230 | key: "id", 231 | attributes: { 232 | root: leafLevel.inputProperty("id") 233 | }, 234 | dataKey: "author" 235 | 236 | }, { 237 | key: "representedOrganization", 238 | content: [{ 239 | key: "id", 240 | attributes: { 241 | root: leafLevel.inputProperty("id") 242 | }, 243 | dataKey: "author" 244 | }, { 245 | key: "name", 246 | text: leafLevel.inputProperty("name"), 247 | dataKey: "author" 248 | 249 | }] 250 | }] 251 | } 252 | ], 253 | dataKey: "meta.ccda_header" 254 | }; 255 | var headerInformant = exports.headerInformant = { 256 | key: "informant", 257 | content: { 258 | key: "assignedEntity", 259 | //attributes: {id:} 260 | content: [{ 261 | key: "id", 262 | attributes: { 263 | root: leafLevel.inputProperty("id") 264 | }, 265 | dataKey: "informant" 266 | 267 | }, { 268 | key: "representedOrganization", 269 | content: [{ 270 | key: "id", 271 | attributes: { 272 | root: leafLevel.inputProperty("id") 273 | }, 274 | dataKey: "informant" 275 | }, { 276 | key: "name", 277 | text: leafLevel.inputProperty("name"), 278 | dataKey: "informant" 279 | 280 | }] 281 | }] 282 | }, 283 | dataKey: "meta.ccda_header" 284 | }; 285 | var headerCustodian = exports.headerCustodian = { 286 | key: "custodian", 287 | content: { 288 | key: "assignedCustodian", 289 | //attributes: {id:} 290 | content: [{ 291 | key: "representedCustodianOrganization", 292 | content: [{ 293 | key: "id", 294 | attributes: { 295 | root: leafLevel.inputProperty("id") 296 | }, 297 | dataKey: "custodian" 298 | }, { 299 | key: "name", 300 | text: leafLevel.inputProperty("name"), 301 | dataKey: "custodian" 302 | 303 | }] 304 | }] 305 | }, 306 | dataKey: "meta.ccda_header" 307 | }; 308 | 309 | var providers = exports.providers = { 310 | key: "documentationOf", 311 | attributes: { 312 | typeCode: "DOC" 313 | }, 314 | content: { 315 | key: "serviceEvent", 316 | attributes: { 317 | classCode: "PCPR" 318 | }, 319 | content: [ 320 | provider 321 | ] 322 | }, 323 | dataKey: "data.demographics" 324 | }; 325 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Amida Technology Solutions (http://amida-tech.com) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /test/xmlmods/viteraParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var t = require("./templatePath"); 4 | 5 | var titleMap = { 6 | "Allergies": "Allergies, adverse reactions, alerts", 7 | "Medications": "History of medication use", 8 | "Procedures and Surgical/Medical History": "History of Procedures", 9 | "Insurance": "Payers", 10 | "Problems": "Problem List", 11 | "SOCIAL HISTORY": "Social History", 12 | "Lab Results": "Relevant diagnostic tests and/or laboratory data" 13 | }; 14 | 15 | var normalizedCodeSystemNames = { 16 | "RxNorm": "RXNORM", 17 | "CPT-4": "CPT", 18 | "RoleClassRelationshipFormal": "HL7 RoleCode", 19 | "RoleCode": "HL7 Role", 20 | "ICD9CM": "ICD-9-CM", 21 | "AdministrativeGender": "HL7 AdministrativeGender", 22 | "MaritalStatus": "HL7 Marital Status", 23 | "CDC Race and Ethnicity": "Race and Ethnicity - CDC" 24 | }; 25 | 26 | var normalizedDisplayNames = { 27 | "HISTORY OF MEDICATION USE": "History of medication use", 28 | "HISTORY OF IMMUNIZATIONS": "Immunizations", 29 | "HISTORY OF PROCEDURES": "History of Procedures", 30 | "history of prior surgery [For Hx of Tx, use H prefix]": "history of prior surgery [For Hx of Tx, use H prefix]", 31 | "History of encounters": "Encounters", 32 | "PAYMENT SOURCES": "Payment sources", 33 | "TREATMENT PLAN": "Plan of Care", 34 | "Problem list": "Problem List", 35 | "VITAL SIGNS": "Vital Signs", 36 | "RESULTS": "Relevant diagnostic tests and/or laboratory data" 37 | }; 38 | 39 | module.exports = [{ 40 | xpath: "//h:name[not(h:family)][not(text())]", 41 | action: "removeNode", 42 | comment: "bunch of empty names to be investigated" 43 | }, { 44 | xpath: "//h:effectiveTime[not(*)][not(@*)]", 45 | action: "removeNode", 46 | comment: "all childless and attributeless times (maybe previously removed nullFlavor)" 47 | }, { 48 | xpath: "//h:assignedPerson[not(*)]", 49 | action: "removeNode", 50 | comment: "all childless and attributeless assignedPerson (maybe previously removed nullFlavor)" 51 | }, { 52 | xpath: t.allergiesSection + '/.//h:effectiveTime[not(@value | h:low | h:high)]', 53 | action: "removeNode", 54 | }, { 55 | xpath: t.allergiesSection + '/h:id', 56 | action: "removeNode", 57 | comment: "error in file: id does not exist in spec" 58 | }, { 59 | xpath: t.allergyObs + '/..', 60 | action: "addAttribute", 61 | params: { 62 | "inversionInd": "true" 63 | }, 64 | comment: "parser expects a value", 65 | }, { 66 | xpath: t.allergyObs + '/h:informant', 67 | action: "removeNode", 68 | comment: "error in file: informant does not exist in spec", 69 | }, { 70 | xpath: t.allergyObs + '/h:participant/h:participantRole/h:playingEntity/h:name', 71 | action: "removeNode", 72 | comment: "needs to be researched" 73 | }, { 74 | xpath: t.allergyReaction + '/h:code', 75 | action: "removeNode", 76 | comment: "can be anything according to spec and parser does not read it" 77 | }, { 78 | xpath: t.allergyCommentAct, 79 | action: "removeNode", 80 | comment: "error in file: Ignoring Comment Activity" 81 | }, { 82 | xpath: t.medSection + '/h:id', 83 | action: "removeNode", 84 | comment: "error in file: id does not exist in spec" 85 | }, { 86 | xpath: t.medActivity + '/h:effectiveTime[@operator="A"]', 87 | action: "removeNode", 88 | comment: "error in file: unexpected interval" 89 | }, { 90 | xpath: t.medActivity + '/h:informant', 91 | action: "removeNode", 92 | comment: "error in file: no informant node in spec" 93 | }, { 94 | xpath: t.medActivity + '/h:entryRelationship/h:observation[not(h:templateId)]/..', 95 | action: "removeNode", 96 | comment: "error in file: template without templateId" 97 | }, { 98 | xpath: t.medStatus, 99 | action: "removeNode", 100 | comment: "error in file: C32 template not valid in CCDA" 101 | }, { 102 | xpath: t.medProbAct, 103 | action: "removeNode", 104 | comment: "error in file: there is no Problem Act in meidcations" 105 | }, { 106 | xpath: t.medDispenseInfo, 107 | action: "removeNode", 108 | comment: "parser does not read" 109 | }, { 110 | xpath: t.medSupplyInfo, 111 | action: "removeNode", 112 | comment: "parser does not read" 113 | }, { 114 | xpath: t.medActivityInfo + '/h:manufacturedMaterial/h:name', 115 | action: "removeNode", 116 | comment: "parser does not read" 117 | }, { 118 | xpath: t.medSupplyOrder + '/h:id', 119 | action: "removeNode", 120 | comment: "parser does not read" 121 | }, { 122 | xpath: t.medSupplyOrder + '/h:effectiveTime', 123 | action: "removeNode", 124 | comment: "no value" 125 | }, { 126 | xpath: t.medSupplyOrder + '/h:author/h:assignedAuthor/h:addr', 127 | action: "removeNode", 128 | comment: "parser does not read" 129 | }, { 130 | xpath: t.medSupplyOrder + '/h:quantity[@unit]', 131 | action: "removeAttribute", 132 | params: "unit", 133 | comment: "parser does not read" 134 | }, { 135 | xpath: t.immSection + '/h:id', 136 | action: "removeNode", 137 | comment: "error in file: id does not exist in spec" 138 | }, { 139 | xpath: t.immActivity + '/h:code', 140 | action: "removeNode", 141 | comment: "parser does not read" 142 | }, { 143 | xpath: t.immActivity + '/h:consumable/h:manufacturedProduct/h:manufacturedMaterial/h:name', 144 | action: "removeNode", 145 | comment: "to be researched" 146 | }, { 147 | xpath: t.immActivity + '/h:consumable/h:manufacturedProduct/h:manufacturerOrganization/h:standardIndustryClassCode', 148 | action: "removeNode", 149 | comment: "to be researched" 150 | }, { 151 | xpath: t.immActivity + '/h:informant', 152 | action: "removeNode", 153 | comment: "to be researched" 154 | }, { 155 | xpath: t.immActUnknown1, 156 | action: "removeNode", 157 | comment: "unknown CCDA templateId" 158 | }, { 159 | xpath: t.immActUnknown2, 160 | action: "removeNode", 161 | comment: "unknown CCDA templateId" 162 | }, { 163 | xpath: t.immActComment + '/h:act', 164 | action: "addAttribute", 165 | params: { 166 | "moodCode": "INT" 167 | }, 168 | comment: "just change ...22.4.64 is not good anyway" 169 | }, { 170 | xpath: t.immActComment + '/h:act/h:templateId', 171 | action: "addAttribute", 172 | params: { 173 | "root": "2.16.840.1.113883.10.20.22.4.20" 174 | }, 175 | comment: "2.16.840.1.113883.10.20.22.4.64 (comment) or 2.16.840.1.113883.10.20.22.4.20" 176 | }, { 177 | xpath: t.procSection + '/h:id', 178 | action: "removeNode", 179 | comment: "error in file: id does not exist in spec" 180 | }, { 181 | xpath: t.procActProc + '/h:informant', 182 | action: "removeNode", 183 | comment: "to be researched" 184 | }, { 185 | xpath: t.procActProc + '/h:participant/h:templateId', 186 | action: "removeNode", 187 | comment: "error in file: this should be in participantRole" 188 | }, { 189 | xpath: t.procActProc + '/h:participant/h:participantRole/h:id', 190 | action: "removeNode", 191 | comment: "to be researched" 192 | }, { 193 | xpath: t.procActProcUnknown, 194 | action: "removeNode", 195 | comment: "to be researched" 196 | }, { 197 | xpath: t.encSection + '/h:id', 198 | action: "removeNode", 199 | comment: "error in file: id does not exist in spec" 200 | }, { 201 | xpath: t.encAct + '/h:informant', 202 | action: "removeNode", 203 | comment: "to be researched" 204 | }, { 205 | xpath: t.encAct + '/h:participant/h:templateId', 206 | action: "removeNode", 207 | comment: "error in file: this should be in participantRole" 208 | }, { 209 | xpath: t.encAct + '/h:participant/h:participantRole/h:id', 210 | action: "removeNode", 211 | comment: "to be researched" 212 | }, { 213 | xpath: t.payersSection + '/h:id', 214 | action: "removeNode", 215 | comment: "error in file: id does not exist in spec" 216 | }, { 217 | xpath: t.payersSection + '/h:code', 218 | action: "addAttribute", 219 | params: { 220 | "displayName": "Payers" 221 | } 222 | }, { 223 | xpath: t.coverageAct + '/h:informant', 224 | action: "removeNode", 225 | comment: "to be researched" 226 | }, { 227 | xpath: t.coverageAct + '/h:entryRelationship/h:sequenceNumber', 228 | action: "removeNode", 229 | comment: "to be researched" 230 | }, { 231 | xpath: t.policyAct + '/h:entryRelationship/h:act[@moodCode="DEF"]', 232 | action: "addAttribute", 233 | params: { 234 | "moodCode": "EVN" 235 | }, 236 | comment: "to be researched" 237 | }, { 238 | xpath: t.policyAct + '/h:participant/h:participantRole/h:playingEntity/*[@value="19381212"]', 239 | action: "removeNode", 240 | comment: "to be researched" 241 | }, { 242 | xpath: t.policyAct + '/h:performer/h:assignedEntity/h:representedOrganization[not(*)]', 243 | action: "removeNode", 244 | comment: "to be researched" 245 | }, { 246 | xpath: t.pocSection + '/h:id', 247 | action: "removeNode", 248 | comment: "error in file: id does not exist in spec" 249 | }, { 250 | xpath: t.pocActProc, 251 | action: "addAttribute", 252 | params: { 253 | "moodCode": "RQO" 254 | }, 255 | comment: "parser does not support" 256 | }, { 257 | xpath: t.pocActProcUnknown, 258 | action: "removeNode", 259 | comment: "not clear in specification, parser does not read" 260 | }, { 261 | xpath: t.pocActProc + '/h:performer', 262 | action: "removeNode", 263 | comment: "not clear in specification, parser does not read" 264 | }, { 265 | xpath: t.probSection + '/h:id', 266 | action: "removeNode", 267 | comment: "error in file: id does not exist in spec" 268 | }, { 269 | xpath: t.probAct + '/h:statusCode', 270 | action: "addAttribute", 271 | params: { 272 | "code": "completed" 273 | }, 274 | comment: "parser deficiency: not read" 275 | }, { 276 | xpath: t.probAct + '/h:performer', 277 | action: "removeNode", 278 | comment: "invalid" 279 | }, { 280 | xpath: t.probObservation + '/h:informant', 281 | action: "removeNode", 282 | comment: "invalid" 283 | }, { 284 | xpath: t.probObservation + '/h:code', 285 | action: "removeNode", 286 | }, { 287 | xpath: t.probObservation + '/..', 288 | action: "removeAttribute", 289 | params: "inversionInd", 290 | }, { 291 | xpath: t.probStatus + '/..', 292 | action: "removeAttribute", 293 | params: "inversionInd", 294 | }, { 295 | xpath: t.probStatus + '/h:value', 296 | action: "addAttribute", 297 | params: { 298 | "xsi:type": "CD" 299 | } 300 | }, { 301 | xpath: t.probActComment, 302 | action: "removeNode", 303 | comment: "Comment Activity is not implemented by Parser" 304 | }, { 305 | xpath: t.resultsSection + '/h:id', 306 | action: "removeNode", 307 | comment: "error in file: id does not exist in spec" 308 | }, { 309 | xpath: t.resultsSection + '/h:entry', 310 | action: "addAttribute", 311 | params: { 312 | "typeCode": "DRIV" 313 | } 314 | }, { 315 | xpath: t.resultOrg + '/h:participant', 316 | action: "removeNode" 317 | }, { 318 | xpath: t.resultOrg + '/h:component/h:procedure', 319 | action: "removeNode" 320 | }, { 321 | xpath: t.resultOrg + '/h:specimen', 322 | action: "removeNode" 323 | }, { 324 | xpath: t.resultOrg + '/h:effectiveTime', 325 | action: "removeNode" 326 | }, { 327 | xpath: t.resultObs + '/h:performer', 328 | action: "removeNode" 329 | }, { 330 | xpath: t.resultObs + '/h:value[@value]', 331 | action: "removeZeros" 332 | }, { 333 | xpath: t.resultsCommentAct, 334 | action: "removeNode", 335 | comment: "error in file: Ignoring Comment Activity" 336 | }, { 337 | xpath: t.vitalsSection + '/h:id', 338 | action: "removeNode", 339 | comment: "error in file: id does not exist in spec" 340 | }, { 341 | xpath: t.vitalsObs + '/h:informant', 342 | action: "removeNode" 343 | }, { 344 | xpath: t.vitalsObs + '/h:methodCode', 345 | action: "removeNode" 346 | }, { 347 | xpath: t.resultsSection + "/.//*[not(*)][not(@*)][not(text())]", 348 | action: "removeNode" 349 | }, { 350 | xpath: "//h:title", 351 | action: "replaceText", 352 | params: titleMap, 353 | comment: "titles may differ" 354 | }, { 355 | xpath: "//h:recordTarget/h:patientRole/h:patient/h:name", 356 | action: "addAttribute", 357 | params: { 358 | "use": "L" 359 | }, 360 | comment: "parser does read @use and generator assumes it is always 'L'" 361 | }, { 362 | xpath: "//*[@codeSystem][@codeSystemName]", 363 | action: "normalize", 364 | params: { 365 | attr: "codeSystemName", 366 | map: normalizedCodeSystemNames 367 | }, 368 | comment: 'blue-button parser normalization' 369 | }, { 370 | xpath: "//*[@codeSystem][@displayName][@code]", 371 | action: "normalize", 372 | params: { 373 | attr: "displayName", 374 | srcAttr: "code", 375 | map: normalizedDisplayNames 376 | }, 377 | comment: 'blue-button parser normalization' 378 | }]; 379 | -------------------------------------------------------------------------------- /lib/sectionLevel2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('lodash'); 4 | var bbu = require("@amida-tech/blue-button-util"); 5 | 6 | var fieldLevel = require("./fieldLevel"); 7 | var entryLevel = require("./entryLevel"); 8 | var leafLevel = require('./leafLevel'); 9 | var contentModifier = require("./contentModifier"); 10 | 11 | var required = contentModifier.required; 12 | var bbuo = bbu.object; 13 | 14 | var nda = "No Data Available"; 15 | 16 | var condition = require('./condition'); 17 | 18 | var getText = function (topArrayKey, headers, values) { 19 | var result = { 20 | key: "text", 21 | existsWhen: condition.keyExists(topArrayKey), 22 | 23 | content: [{ 24 | key: "table", 25 | attributes: { 26 | border: "1", 27 | width: "100%" 28 | }, 29 | content: [{ 30 | key: "thead", 31 | content: [{ 32 | key: "tr", 33 | content: [] 34 | }] 35 | }, { 36 | key: "tbody", 37 | content: [{ 38 | key: "tr", 39 | content: [], 40 | dataKey: topArrayKey 41 | }] 42 | }] 43 | }] 44 | }; 45 | var headerTarget = result.content[0].content[0].content[0].content; 46 | headers.forEach(function (header) { 47 | var element = { 48 | key: "th", 49 | text: header 50 | }; 51 | headerTarget.push(element); 52 | }); 53 | var valueTarget = result.content[0].content[1].content[0].content; 54 | values.forEach(function (value) { 55 | var data; 56 | if (typeof value !== 'function') { 57 | data = leafLevel.deepInputProperty(value, ""); 58 | } else { 59 | data = value; 60 | } 61 | 62 | var element = { 63 | key: "td", 64 | text: data 65 | }; 66 | valueTarget.push(element); 67 | }); 68 | return result; 69 | }; 70 | 71 | var alllergiesTextHeaders = ["Substance", "Overall Severity", "Reaction", "Reaction Severity", "Status"]; 72 | var allergiesTextRow = [ 73 | leafLevel.deepInputProperty("observation.allergen.name", ""), 74 | leafLevel.deepInputProperty("observation.severity.code.name", ""), 75 | leafLevel.deepInputProperty("observation.reactions.0.reaction.name", ""), 76 | leafLevel.deepInputProperty("observation.reactions.0.severity.code.name", ""), 77 | leafLevel.deepInputProperty("observation.status.name", "") 78 | ]; 79 | 80 | exports.allergiesSectionEntriesRequired = function (htmlHeader, na) { 81 | return { 82 | key: "component", 83 | content: [{ 84 | key: "section", 85 | content: [ 86 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.6"), 87 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.6.1"), 88 | fieldLevel.templateCode("AllergiesSection"), 89 | fieldLevel.templateTitle("AllergiesSection"), { 90 | key: "text", 91 | text: na, 92 | existsWhen: condition.keyDoesntExist("allergies") 93 | 94 | }, 95 | htmlHeader, { 96 | key: "entry", 97 | attributes: { 98 | "typeCode": "DRIV" 99 | }, 100 | content: [ 101 | [entryLevel.allergyProblemAct, required] 102 | ], 103 | dataKey: "allergies", 104 | required: true 105 | } 106 | ] 107 | }] 108 | }; 109 | }; 110 | 111 | var medicationsTextHeaders = ["Medication Class", "# fills", "Last fill date"]; 112 | var medicationsTextRow = [ // Name, did not find class in the medication blue-button-data 113 | function (input) { 114 | var value = _.get(input, 'product.product.name'); 115 | if (!bbuo.exists(value)) { 116 | value = _.get(input, 'product.unencoded_name'); 117 | } 118 | if (!bbuo.exists(value)) { 119 | return ""; 120 | } else { 121 | return value; 122 | } 123 | }, 124 | leafLevel.deepInputProperty("supply.repeatNumber", ""), 125 | leafLevel.deepInputDate("supply.date_time.point", "") 126 | ]; 127 | 128 | exports.medicationsSectionEntriesRequired = function (htmlHeader, na) { 129 | return { 130 | key: "component", 131 | content: [{ 132 | key: "section", 133 | content: [ 134 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.1"), 135 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.1.1"), 136 | fieldLevel.templateCode("MedicationsSection"), 137 | fieldLevel.templateTitle("MedicationsSection"), { 138 | key: "text", 139 | text: na, 140 | existsWhen: condition.keyDoesntExist("medications") 141 | 142 | }, 143 | htmlHeader, { 144 | key: "entry", 145 | attributes: { 146 | "typeCode": "DRIV" 147 | }, 148 | content: [ 149 | [entryLevel.medicationActivity, required] 150 | ], 151 | dataKey: "medications", 152 | required: true 153 | } 154 | ] 155 | }] 156 | }; 157 | }; 158 | 159 | exports.problemsSectionEntriesRequired = function (htmlHeader, na) { 160 | return { 161 | key: "component", 162 | content: [{ 163 | key: "section", 164 | content: [ 165 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.5"), 166 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.5.1"), 167 | fieldLevel.templateCode("ProblemSection"), 168 | fieldLevel.templateTitle("ProblemSection"), { 169 | key: "text", 170 | text: na, 171 | existsWhen: condition.keyDoesntExist("problems") 172 | 173 | }, 174 | htmlHeader, { 175 | key: "entry", 176 | attributes: { 177 | "typeCode": "DRIV" 178 | }, 179 | content: [ 180 | [entryLevel.problemConcernAct, required] 181 | ], 182 | dataKey: "problems", 183 | required: true 184 | }, { 185 | 186 | key: "entry", 187 | existsWhen: condition.keyExists("problems_comment"), 188 | content: { 189 | key: "act", 190 | attributes: { 191 | classCode: "ACT", 192 | moodCode: "EVN" 193 | }, 194 | content: [ 195 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.4.64"), 196 | fieldLevel.templateCode("CommentActivity"), { 197 | key: "text", 198 | text: leafLevel.deepInputProperty("problems_comment") 199 | }, 200 | ] 201 | 202 | }, 203 | dataKey: "demographics.meta" 204 | } 205 | ] 206 | }] 207 | }; 208 | }; 209 | 210 | exports.proceduresSectionEntriesRequired = function (htmlHeader, na) { 211 | return { 212 | key: "component", 213 | content: [{ 214 | key: "section", 215 | content: [ 216 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.7"), 217 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.7.1"), 218 | fieldLevel.templateCode("ProceduresSection"), 219 | fieldLevel.templateTitle("ProceduresSection"), { 220 | key: "text", 221 | text: na, 222 | existsWhen: condition.keyDoesntExist("procedures") 223 | 224 | }, 225 | htmlHeader, { 226 | key: "entry", 227 | attributes: { 228 | "typeCode": function (input) { 229 | return input.procedure_type === "procedure" ? "DRIV" : null; 230 | } 231 | }, 232 | content: [ 233 | entryLevel.procedureActivityAct, 234 | entryLevel.procedureActivityProcedure, 235 | entryLevel.procedureActivityObservation 236 | ], 237 | dataKey: "procedures" 238 | } 239 | ] 240 | }], 241 | notImplemented: [ 242 | "entry required" 243 | ] 244 | }; 245 | }; 246 | 247 | exports.resultsSectionEntriesRequired = function (htmlHeader, na) { 248 | return { 249 | key: "component", 250 | content: [{ 251 | key: "section", 252 | content: [ 253 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.3"), 254 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.3.1"), 255 | fieldLevel.templateCode("ResultsSection"), 256 | fieldLevel.templateTitle("ResultsSection"), { 257 | key: "text", 258 | text: na, 259 | existsWhen: condition.keyDoesntExist("results") 260 | 261 | }, 262 | htmlHeader, { 263 | key: "entry", 264 | attributes: { 265 | typeCode: "DRIV" 266 | }, 267 | content: [ 268 | [entryLevel.resultOrganizer, required] 269 | ], 270 | dataKey: "results", 271 | required: true 272 | } 273 | ] 274 | }] 275 | }; 276 | }; 277 | 278 | exports.encountersSectionEntriesOptional = function (htmlHeader, na) { 279 | return { 280 | key: "component", 281 | content: [{ 282 | key: "section", 283 | content: [ 284 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.22"), 285 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.22.1"), 286 | fieldLevel.templateCode("EncountersSection"), 287 | fieldLevel.templateTitle("EncountersSection"), { 288 | key: "text", 289 | text: na, 290 | existsWhen: condition.keyDoesntExist("encounters") 291 | 292 | }, 293 | htmlHeader, { 294 | key: "entry", 295 | attributes: { 296 | "typeCode": "DRIV" 297 | }, 298 | content: [ 299 | [entryLevel.encounterActivities, required] 300 | ], 301 | dataKey: "encounters" 302 | } 303 | ] 304 | }] 305 | }; 306 | }; 307 | 308 | exports.immunizationsSectionEntriesOptional = function (htmlHeader, na) { 309 | return { 310 | key: "component", 311 | content: [{ 312 | key: "section", 313 | content: [ 314 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.2"), 315 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.2.1"), 316 | fieldLevel.templateCode("ImmunizationsSection"), 317 | fieldLevel.templateTitle("ImmunizationsSection"), { 318 | key: "text", 319 | text: na, 320 | existsWhen: condition.keyDoesntExist("immunizations") 321 | 322 | }, { 323 | key: "entry", 324 | attributes: { 325 | "typeCode": "DRIV" 326 | }, 327 | content: [ 328 | [entryLevel.immunizationActivity, required] 329 | ], 330 | dataKey: "immunizations" 331 | } 332 | ] 333 | }] 334 | }; 335 | }; 336 | 337 | exports.payersSection = function (htmlHeader, na) { 338 | return { 339 | key: "component", 340 | content: [{ 341 | key: "section", 342 | content: [ 343 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.18"), 344 | fieldLevel.templateCode("PayersSection"), 345 | fieldLevel.templateTitle("PayersSection"), { 346 | key: "text", 347 | text: na, 348 | existsWhen: condition.keyDoesntExist("payers") 349 | 350 | }, 351 | htmlHeader, { 352 | key: "entry", 353 | attributes: { 354 | typeCode: "DRIV" 355 | }, 356 | content: [ 357 | [entryLevel.coverageActivity, required] 358 | ], 359 | dataKey: "payers" 360 | } 361 | ] 362 | }] 363 | }; 364 | }; 365 | 366 | exports.planOfCareSection = function (htmlHeader, na) { 367 | return { 368 | key: "component", 369 | content: [{ 370 | key: "section", 371 | content: [ 372 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.10"), 373 | fieldLevel.templateCode("PlanOfCareSection"), 374 | fieldLevel.templateTitle("PlanOfCareSection"), { 375 | key: "text", 376 | text: na, 377 | existsWhen: condition.keyDoesntExist("plan_of_care") 378 | 379 | }, 380 | htmlHeader, { 381 | key: "entry", 382 | attributes: { 383 | "typeCode": function (input) { 384 | return input.type === "observation" ? "DRIV" : null; 385 | } 386 | }, 387 | content: [ 388 | entryLevel.planOfCareActivityAct, 389 | entryLevel.planOfCareActivityObservation, 390 | entryLevel.planOfCareActivityProcedure, 391 | entryLevel.planOfCareActivityEncounter, 392 | entryLevel.planOfCareActivitySubstanceAdministration, 393 | entryLevel.planOfCareActivitySupply, 394 | entryLevel.planOfCareActivityInstructions 395 | ], 396 | dataKey: "plan_of_care" 397 | } 398 | ] 399 | }] 400 | }; 401 | }; 402 | 403 | exports.socialHistorySection = function (htmlHeader, na) { 404 | return { 405 | key: "component", 406 | content: [{ 407 | key: "section", 408 | content: [ 409 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.17"), 410 | fieldLevel.templateCode("SocialHistorySection"), 411 | fieldLevel.templateTitle("SocialHistorySection"), { 412 | key: "text", 413 | text: na, 414 | existsWhen: condition.keyDoesntExist("social_history") 415 | 416 | }, { 417 | key: "entry", 418 | attributes: { 419 | typeCode: "DRIV" 420 | }, 421 | content: [ 422 | entryLevel.smokingStatusObservation, 423 | entryLevel.socialHistoryObservation 424 | ], 425 | dataKey: "social_history" 426 | } 427 | ] 428 | }], 429 | notImplemented: [ 430 | "pregnancyObservation", 431 | "tobaccoUse" 432 | ] 433 | }; 434 | }; 435 | 436 | exports.vitalSignsSectionEntriesOptional = function (htmlHeader, na) { 437 | return { 438 | key: "component", 439 | content: [{ 440 | key: "section", 441 | content: [ 442 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.4"), 443 | fieldLevel.templateId("2.16.840.1.113883.10.20.22.2.4.1"), 444 | fieldLevel.templateCode("VitalSignsSection"), 445 | fieldLevel.templateTitle("VitalSignsSection"), { 446 | key: "text", 447 | text: na, 448 | existsWhen: condition.keyDoesntExist("vitals") 449 | 450 | }, { 451 | key: "entry", 452 | attributes: { 453 | typeCode: "DRIV" 454 | }, 455 | content: [ 456 | [entryLevel.vitalSignsOrganizer, required] 457 | ], 458 | dataKey: "vitals" 459 | } 460 | ] 461 | }] 462 | }; 463 | }; 464 | -------------------------------------------------------------------------------- /lib/htmlHeaders.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var bbu = require("@amida-tech/blue-button-util"); 4 | 5 | var fieldLevel = require("./fieldLevel"); 6 | var entryLevel = require("./entryLevel"); 7 | var leafLevel = require('./leafLevel'); 8 | var contentModifier = require("./contentModifier"); 9 | 10 | var required = contentModifier.required; 11 | var bbud = bbu.datetime; 12 | var bbuo = bbu.object; 13 | 14 | var nda = "No Data Available"; 15 | 16 | var condition = require('./condition'); 17 | 18 | var getText = function (topArrayKey, headers, values) { 19 | var result = { 20 | key: "text", 21 | existsWhen: condition.keyExists(topArrayKey), 22 | 23 | content: [{ 24 | key: "table", 25 | attributes: { 26 | border: "1", 27 | width: "100%" 28 | }, 29 | content: [{ 30 | key: "thead", 31 | content: [{ 32 | key: "tr", 33 | content: [] 34 | }] 35 | }, { 36 | key: "tbody", 37 | content: [{ 38 | key: "tr", 39 | content: [], 40 | dataKey: topArrayKey 41 | }] 42 | }] 43 | }] 44 | }; 45 | var headerTarget = result.content[0].content[0].content[0].content; 46 | headers.forEach(function (header) { 47 | var element = { 48 | key: "th", 49 | text: header 50 | }; 51 | headerTarget.push(element); 52 | }); 53 | var valueTarget = result.content[0].content[1].content[0].content; 54 | values.forEach(function (value) { 55 | var data; 56 | if (typeof value !== 'function') { 57 | data = leafLevel.deepInputProperty(value, ""); 58 | } else { 59 | data = value; 60 | } 61 | 62 | var element = { 63 | key: "td", 64 | text: data 65 | }; 66 | valueTarget.push(element); 67 | }); 68 | return result; 69 | }; 70 | 71 | var alllergiesTextHeaders = ["Substance", "Overall Severity", "Reaction", "Reaction Severity", "Status"]; 72 | var allergiesTextRow = [ 73 | leafLevel.deepInputProperty("observation.allergen.name", ""), 74 | leafLevel.deepInputProperty("observation.severity.code.name", ""), 75 | leafLevel.deepInputProperty("observation.reactions.0.reaction.name", ""), 76 | leafLevel.deepInputProperty("observation.reactions.0.severity.code.name", ""), 77 | leafLevel.deepInputProperty("observation.status.name", "") 78 | ]; 79 | 80 | exports.allergiesSectionEntriesRequiredHtmlHeader = getText('allergies', alllergiesTextHeaders, allergiesTextRow); 81 | 82 | var medicationsTextHeaders = ["Medication Class", "# fills", "Last fill date"]; 83 | var medicationsTextRow = [ // Name, did not find class in the medication blue-button-data 84 | function (input) { 85 | var value = input['product.product.name']; 86 | if (!bbuo.exists(value)) { 87 | value = input['product.unencoded_name']; 88 | } 89 | if (!bbuo.exists(value)) { 90 | return ""; 91 | } else { 92 | return value; 93 | } 94 | }, 95 | leafLevel.deepInputProperty("supply.repeatNumber", ""), 96 | leafLevel.deepInputDate("supply.date_time.point", "") 97 | ]; 98 | 99 | exports.medicationsSectionEntriesRequiredHtmlHeader = getText('medications', medicationsTextHeaders, medicationsTextRow); 100 | 101 | exports.problemsSectionEntriesRequiredHtmlHeader = { 102 | key: "text", 103 | existsWhen: condition.keyExists("problems"), 104 | 105 | content: [{ 106 | key: "table", 107 | content: [{ 108 | key: "thead", 109 | content: [{ 110 | key: "tr", 111 | content: { 112 | key: "th", 113 | attributes: { 114 | colspan: "2" 115 | }, 116 | text: "Problems" 117 | } 118 | }, { 119 | key: "tr", 120 | content: [{ 121 | key: "th", 122 | text: leafLevel.input, 123 | dataTransform: function () { 124 | return ['Condition', 'Severity']; 125 | } 126 | }] 127 | }] 128 | }, { 129 | key: "tbody", 130 | content: [{ 131 | key: "tr", 132 | content: [{ 133 | key: "td", 134 | text: leafLevel.deepInputProperty("problem.code.name", nda) 135 | }, { 136 | key: "td", 137 | text: leafLevel.deepInputProperty("problem.severity.code.name", nda) 138 | }] 139 | }], 140 | dataKey: 'problems' 141 | }] 142 | }] 143 | }; 144 | 145 | exports.proceduresSectionEntriesRequiredHtmlHeader = { 146 | 147 | key: "text", 148 | existsWhen: condition.keyExists("procedures"), 149 | 150 | content: [{ 151 | key: "table", 152 | content: [{ 153 | key: "thead", 154 | content: [{ 155 | key: "tr", 156 | content: { 157 | key: "th", 158 | attributes: { 159 | colspan: "5" 160 | }, 161 | text: "Procedures" 162 | } 163 | }, { 164 | key: "tr", 165 | content: [{ 166 | key: "th", 167 | text: leafLevel.input, 168 | dataTransform: function () { 169 | return ['Service', 'Procedure code', 'Service date', 'Servicing provider', 'Phone#']; 170 | } 171 | }] 172 | }] 173 | }, { 174 | key: "tbody", 175 | content: [{ 176 | key: "tr", 177 | content: [{ 178 | key: "td", 179 | text: leafLevel.deepInputProperty("procedure.name", nda) 180 | }, { 181 | key: "td", 182 | text: leafLevel.deepInputProperty("procedure.code", nda) 183 | }, { 184 | key: "td", 185 | text: leafLevel.deepInputDate("date_time.point", nda) 186 | }, { 187 | key: "td", 188 | text: leafLevel.deepInputProperty("performer.0.organization.0.name.0", nda) 189 | }, { 190 | key: "td", 191 | text: leafLevel.deepInputProperty("performer.0.organization.0.phone.0.value.number", nda) 192 | }] 193 | }], 194 | dataKey: 'procedures' 195 | }] 196 | }] 197 | }; 198 | 199 | exports.resultsSectionEntriesRequiredHtmlHeader = { 200 | key: "text", 201 | existsWhen: condition.keyExists("results"), 202 | 203 | content: [{ 204 | key: "table", 205 | content: [{ 206 | key: "thead", 207 | content: [{ 208 | key: "tr", 209 | content: { 210 | key: "th", 211 | attributes: { 212 | colspan: "7" 213 | }, 214 | text: "Laboratory Results" 215 | } 216 | }, { 217 | key: "tr", 218 | content: [{ 219 | key: "th", 220 | text: leafLevel.input, 221 | dataTransform: function () { 222 | return ['Test', 'Result', 'Units', 'Ref low', 'Ref high', 'Date', 'Source']; 223 | } 224 | }] 225 | }] 226 | }, { 227 | key: "tbody", 228 | content: [{ 229 | key: "tr", 230 | content: [{ 231 | key: "td", 232 | attributes: { 233 | colspan: "7" 234 | }, 235 | text: leafLevel.deepInputProperty('result_set.name', nda), 236 | }] 237 | }, { 238 | key: "tr", 239 | content: [{ 240 | key: "td", 241 | text: leafLevel.deepInputProperty("result.name", nda) 242 | }, { 243 | key: "td", 244 | text: leafLevel.deepInputProperty("value", nda) 245 | }, { 246 | key: "td", 247 | text: leafLevel.deepInputProperty("unit", nda) 248 | }, { 249 | key: "td", 250 | text: leafLevel.deepInputProperty("reference_range.low", nda) 251 | }, { 252 | key: "td", 253 | text: leafLevel.deepInputProperty("reference_range.high", nda) 254 | }, { 255 | key: "td", 256 | text: leafLevel.deepInputDate("date_time.point", nda), 257 | }, { 258 | key: "td", 259 | text: nda 260 | }], 261 | dataKey: 'results' 262 | }], 263 | dataKey: 'results' 264 | }] 265 | }] 266 | }; 267 | 268 | exports.encountersSectionEntriesOptionalHtmlHeader = { 269 | key: "text", 270 | existsWhen: condition.keyExists("encounters"), 271 | 272 | content: [{ 273 | key: "table", 274 | content: [{ 275 | key: "caption", 276 | text: "Encounters" 277 | }, { 278 | key: "thead", 279 | content: [{ 280 | key: "tr", 281 | content: [{ 282 | key: "th", 283 | text: leafLevel.input, 284 | dataTransform: function () { 285 | return ['Type', 'Facility', 'Date of Service', 'Diagnosis/Complaint']; 286 | } 287 | }] 288 | }] 289 | }, { 290 | key: "tbody", 291 | content: [{ 292 | key: "tr", 293 | content: [{ 294 | key: "td", 295 | text: leafLevel.deepInputProperty("encounter.name", nda) 296 | }, { 297 | key: "td", 298 | text: leafLevel.deepInputProperty("locations.0.name", nda) 299 | }, { 300 | key: "td", 301 | text: function (input) { 302 | var value = input["date_time.point"]; 303 | if (value) { 304 | value = bbud.modelToDate({ 305 | date: value.date, 306 | precision: value.precision // workaround a bug in bbud. Changes precision. 307 | }); 308 | if (value) { 309 | var vps = value.split('-'); 310 | if (vps.length === 3) { 311 | return [vps[1], vps[2], vps[0]].join('/'); 312 | } 313 | } 314 | } 315 | return nda; 316 | } 317 | }, { 318 | key: "td", 319 | text: leafLevel.deepInputProperty("findings.0.value.name", nda) 320 | }], 321 | }], 322 | dataKey: 'encounters' 323 | }] 324 | }] 325 | }; 326 | 327 | exports.immunizationsSectionEntriesOptionalHtmlHeader = {}; 328 | 329 | exports.payersSectionHtmlHeader = { 330 | key: "text", 331 | existsWhen: condition.keyExists("payers"), 332 | 333 | content: [{ 334 | key: "table", 335 | content: [{ 336 | key: "thead", 337 | content: [{ 338 | key: "tr", 339 | content: { 340 | key: "th", 341 | attributes: { 342 | colspan: "5" 343 | }, 344 | text: "Payers" 345 | } 346 | }, { 347 | key: "tr", 348 | content: [{ 349 | key: "th", 350 | text: leafLevel.input, 351 | dataTransform: function () { 352 | return ['Payer Name', 'Group ID', 'Member ID', 'Elegibility Start Date', 'Elegibility End Date']; 353 | } 354 | }] 355 | }] 356 | }, { 357 | key: "tbody", 358 | content: [{ 359 | key: "tr", 360 | content: [{ 361 | key: "td", 362 | text: leafLevel.deepInputProperty("policy.insurance.performer.organization.0.name.0", nda) 363 | }, { 364 | key: "td", 365 | text: leafLevel.deepInputProperty("policy.identifiers.0.extension", nda) 366 | }, { 367 | key: "td", 368 | text: leafLevel.deepInputProperty("participant.performer.identifiers.0.extension", nda) 369 | }, { 370 | key: "td", 371 | text: leafLevel.deepInputProperty("participant.date_time.low.date", nda) 372 | }, { 373 | key: "td", 374 | text: leafLevel.deepInputProperty("participant.date_time.high.date", nda) 375 | }] 376 | }], 377 | dataKey: 'payers' 378 | }] 379 | }] 380 | }; 381 | 382 | exports.planOfCareSectionHtmlHeader = { 383 | key: "text", 384 | existsWhen: condition.keyExists("plan_of_care"), 385 | content: [{ 386 | key: "table", 387 | content: [{ 388 | key: "thead", 389 | content: [{ 390 | key: "tr", 391 | content: { 392 | key: "th", 393 | attributes: { 394 | colspan: "4" 395 | }, 396 | text: "Plan of Care" 397 | } 398 | }, { 399 | key: "tr", 400 | content: [{ 401 | key: "th", 402 | text: leafLevel.input, 403 | dataTransform: function () { 404 | return ['Program', 'Start Date', 'Severity', '']; 405 | } 406 | }] 407 | }] 408 | }, { 409 | key: "tbody", 410 | content: [{ 411 | key: "tr", 412 | content: [{ 413 | key: "td", 414 | text: leafLevel.deepInputProperty("plan.name", nda) 415 | }, { 416 | key: "td", 417 | text: leafLevel.deepInputDate("date_time.low", nda) 418 | }, { 419 | key: "td", 420 | text: leafLevel.deepInputProperty("severity.name", nda) 421 | }, { 422 | key: "td", 423 | content: { 424 | 425 | key: "table", 426 | content: [{ 427 | key: "thead", 428 | content: [{ 429 | key: "tr", 430 | content: [{ 431 | key: "th", 432 | text: leafLevel.input, 433 | dataTransform: function () { 434 | return ['Goals', '']; 435 | } 436 | }] 437 | }] 438 | }, { 439 | key: "tbody", 440 | content: [{ 441 | key: "tr", 442 | content: [{ 443 | key: "td", 444 | text: leafLevel.deepInputProperty("goal.name", nda) 445 | }, { 446 | key: "td", 447 | content: { 448 | 449 | key: "table", 450 | content: [{ 451 | key: "thead", 452 | content: [{ 453 | key: "tr", 454 | content: [{ 455 | key: "th", 456 | text: leafLevel.input, 457 | dataTransform: function () { 458 | return ['Interventions']; 459 | } 460 | }] 461 | }] 462 | }, { 463 | key: "tbody", 464 | content: [{ 465 | key: "tr", 466 | content: [{ 467 | key: "td", 468 | text: leafLevel.deepInputProperty("intervention.name", nda) 469 | }] 470 | }], 471 | dataKey: 'interventions' 472 | }] 473 | 474 | } 475 | 476 | }] 477 | }], 478 | dataKey: 'goals' 479 | }] 480 | } 481 | 482 | }] 483 | }], 484 | dataKey: 'plan_of_care' 485 | }] 486 | }] 487 | }; 488 | 489 | exports.socialHistorySectionHtmlHeader = {}; 490 | 491 | exports.vitalSignsSectionEntriesOptionalHtmlHeader = {}; 492 | 493 | exports.allergiesSectionEntriesRequiredHtmlHeaderNA = "Not Available"; 494 | exports.medicationsSectionEntriesRequiredHtmlHeaderNA = "Not Available"; 495 | exports.problemsSectionEntriesRequiredHtmlHeaderNA = "Not Available"; 496 | exports.proceduresSectionEntriesRequiredHtmlHeaderNA = "Not Available"; 497 | exports.resultsSectionEntriesRequiredHtmlHeaderNA = "Not Available"; 498 | exports.encountersSectionEntriesOptionalHtmlHeaderNA = "Not Available"; 499 | exports.immunizationsSectionEntriesOptionalHtmlHeaderNA = "Not Available"; 500 | exports.payersSectionHtmlHeaderNA = "Not Available"; 501 | exports.planOfCareSectionHtmlHeaderNA = "Not Available"; 502 | exports.socialHistorySectionHtmlHeaderNA = "Not Available"; 503 | exports.vitalSignsSectionEntriesOptionalHtmlHeaderNA = "Not Available"; 504 | --------------------------------------------------------------------------------