Skip to content

Commit

Permalink
Merge pull request #7 from cqframework/support-urns-urls-oids
Browse files Browse the repository at this point in the history
Support URNs, URLs, and OIDs
  • Loading branch information
Dylan Mahalingam authored Apr 24, 2020
2 parents 4d5318d + 262cecf commit af64d01
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 92 deletions.
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

This project establishes a VSAC-enabled code service module for use with the CQL Execution Engine. This allows the
CQL Execution Engine to execute CQL containing references to Value Sets that are published in the National Library of
Medicine's (NLM) Value Set Authority Center (VSAC). Value Set references should be defined using OIDS. For example:
Medicine's (NLM) Value Set Authority Center (VSAC). Value Set references can be defined using a valid VSAC
identifying URL for the value set, a URN, or the oid itself. For example:

```
valueset "Diabetes": '2.16.840.1.113883.3.464.1003.103.12.1001'
valueset "Diabetes": 'https://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.103.12.1001'
// or valueset "Diabetes": 'urn:oid:2.16.840.1.113883.3.464.1003.103.12.1001'
// or valueset "Diabetes": '2.16.840.1.113883.3.464.1003.103.12.1001'
```

This library requires that the credentials of a valid UMLS account be provided to it. If you do not have an UMLS
Expand Down Expand Up @@ -41,13 +44,15 @@ provided via `UMLS_USER_NAME` and `UMLS_PASSWORD` environment variables.

## Downloading Value Set Definitions

The `ensureValueSets` function is the only function that attempts to download value sets from VSAC. Before it makes
a request to VSAC, it will check the cache. If the value set is already in the cache, it will not make a request to
VSAC. Otherwise, it will use VSAC's SVS2 API to download the expanded codes from the value set.
The `ensureValueSets` and `ensureValueSetsInLibrary` functions are the only functions that attempt to download value
sets from VSAC. Before they make a request to VSAC, they will check the cache. If the value set is already in the
cache, they will not make a request to VSAC. Otherwise, they will use VSAC's SVS2 API to download the expanded codes
from the value set.

The `findValueSetsByOID` and `findValueSet` functions do not reach out to VSAC, so implementations should call
`ensureValueSets` with the value set OIDs / versions first, before attempting to execute CQL. Usually an implementor
will extract the list of value set OIDs / versions from the ELM to pass them into `ensureValueSets`.
The `findValueSet` and `findValueSets` functions (including the legacy `findValueSetsByOid` function) do not reach out
to VSAC, so implementations should call `ensureValueSetsInLibrary` or `ensureValueSets` before attempting to execute
CQL. If `ensureValueSets` is used, the implementor is responsible for passing in the OIDs / versions that will be
needed.

## Example

Expand All @@ -60,17 +65,10 @@ const vsac = require('cql-exec-vsac');
// Code setting up the CQL library, patient source, parameters, etc
// ...

// Extract the value sets from the ELM
let valueSets = [];
const json = JSON.parse(fs.readFileSync('/path/to/cdsELM.json', 'utf8'));
if (json.library && json.library.valueSets && json.library.valueSets.def) {
valueSets = json.library.valueSets.def;
}

// Set up the code service, loading from the cache if it exists
const codeService = new vsac.CodeService('/path/to/vsac_cache', true);
// Ensure value sets, downloading any missing value sets
codeService.ensureValueSets(valueSets, 'myUMLSUserName', 'myUMLSPassword')
// Ensure value sets in the library and its dependencies, downloading any missing value sets
codeService.ensureValueSetsInLibrary(library, true, 'myUMLSUserName', 'myUMLSPassword')
.then(() => {
// Value sets are loaded, so execute!
const executor = new cql.Executor(lib, codeService, parameters);
Expand Down
79 changes: 53 additions & 26 deletions lib/CodeService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const proc = require('process');
const env = proc.env;
const path = require('path');
const {downloadFromVSAC} = require('./download-vsac');
const extractOidAndVersion = require('./extractOidAndVersion');

/**
* Constructs a code service with functions for downloading codes from the National Library of Medicine's
Expand Down Expand Up @@ -98,39 +99,65 @@ class CodeService {
return this.ensureValueSets(Array.from(valueSets), umlsUserName, umlsPassword, caching);
}

/**
* The findValueSetsByOid function is kept for backwards compatibility (and since cql-execution
* calls it), but now it just calls the more appropriately named findValuesets.
* @param {string} oid - the OID to lookup the VS by (note: this now also support URN and URL)
* @returns {Array<Object>} a list of the matching value sets
*/
findValueSetsByOid(oid) {
return this.findValueSets(oid);
}

/**
* Returns a list of value sets matching the passed in identifier. If version is passed in, it will also filter
* by version (resulting in a list of, at most, 1 item). If no version is passed in, all versions are returned.
* Note that this does not do any network calls -- it operated only on the cached value sets.
* @param {string} id - the identifier for the value set (may be OID, URN, or VSAC FHIR URL)
* @param {string} version - the optional version; if blank, returns all versions
* @returns {Array<Object>} a list of the matching value sets
*/
findValueSets(id, version) {
const result = [];
const [oid, embeddedVersion] = extractOidAndVersion(id);
if (version == null && embeddedVersion != null) {
version = embeddedVersion;
}
const vs = this.valueSets[oid];
for (let version in vs) {
result.push(vs[version]);
if (vs) {
for (let foundVersion in vs) {
if (version == null || foundVersion === version) {
result.push(vs[foundVersion]);
}
}
}
return result;
}

findValueSet(oid, version) {
if (version != null) {
const vsObj = this.valueSets[oid];
if (typeof vsObj !== 'undefined') {
return vsObj[version];
} else {
return;
}
}
else {
const results = this.findValueSetsByOid(oid);
if (results.length === 0) {
return;
}
else {
return results.reduce(function(a, b) {
if (a.version > b.version) {
return a;
}
else {
return b;
}
});
}
/**
* Returns the value set matching the passed in identifier and version (if applicable). If no version is
* passed in, and multiple versions are found, it will attempt to return the most recent version using a
* simple string comparison of versions. Note that this does not do any network calls -- it operates only
* on the cached value sets.
* @param {string} id - the identifier for the value set (may be OID, URN, or VSAC FHIR URL)
* @param {string} version - the optional version; if blank, attempts to return the most recent version
* @returns {Object} the matching value set or undefined (if no match is found)
*/
findValueSet(id, version) {
const results = this.findValueSets(id, version);
if (results.length === 0) {
return;
} else if (results.length === 1) {
return results[0];
} else {
return results.reduce(function(a, b) {
if (a.version > b.version) {
return a;
}
else {
return b;
}
});
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions lib/download-vsac.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const rpn = require('request-promise-native');
const mkdirp = require('mkdirp');
const parseVSACXML = require('./parse-vsac');
const extractOidAndVersion = require('./extractOidAndVersion');
const debug = require('debug')('vsac'); // To turn on DEBUG: $ export DEBUG=vsac

function downloadFromVSAC(username, password, input, output, vsDB={}, caching=true) {
Expand All @@ -12,9 +13,10 @@ function downloadFromVSAC(username, password, input, output, vsDB={}, caching=tr
vsJSON = require(input);
} else {
var keys = Object.keys(input);
keys.forEach(function(val,idx) {
if (!(input[val].id in vsDB)) {
vsJSON[input[val].name] = input[val].id;
keys.forEach(function(val) {
const [oid] = extractOidAndVersion(input[val].id);
if (!(oid in vsDB)) {
vsJSON[input[val].name] = oid;
}
});
}
Expand Down
23 changes: 23 additions & 0 deletions lib/extractOidAndVersion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Extracts just the oid from a urn, url, or oid. If it is not a valid urn or VSAC URL,
* it is assumed to be an oid and returned as-is.
* @param {string} id - the urn, url, or oid
* @returns {string} the oid
*/
function extractOidAndVersion(id) {
if (id == null) return [];

// first check for VSAC FHIR URL (ideally https is preferred but support http just in case)
// if there is a | at the end, it indicates that a version string follows
let m = id.match(/^https?:\/\/cts\.nlm\.nih\.gov\/fhir\/ValueSet\/([^|]+)(\|(.+))?$/);
if (m) return m[3] == null ? [m[1]] : [m[1], m[3]];

// then check for urn:oid
m = id.match(/^urn:oid:(.+)$/);
if (m) return [m[1]];

// finally just return as-is
return [id];
}

module.exports = extractOidAndVersion;
11 changes: 11 additions & 0 deletions test/fixtures/valueset-db.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@
"version": "2.58"
}
]
},
"20200401": {
"oid": "2.16.840.1.113883.3.464.1003.104.12.1013",
"version": "20200401",
"codes": [
{
"code": "48620-9",
"system": "http://loinc.org",
"version": "2.58"
}
]
}
}
}
Loading

0 comments on commit af64d01

Please sign in to comment.