Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This server provides a set of server-side services that are useful for the FHIR

## Services useful the community as a whole

* [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig)(as running at http://tx.fhir.org/tx-reg)
* [TX Registry](registry/readme.md) - **Terminology System Registry** as [described by the terminology ecosystem specification](https://build.fhir.org/ig/HL7/fhir-tx-ecosystem-ig) (as running at http://tx.fhir.org/tx-reg)
* [Package server](packages/readme.md) - **NPM-style FHIR package registry** with search, versioning, and downloads, consistent with the FHIR NPM Specification (as running at http://packages2.fhir.org/packages)
* [XIG server](xig/readme.md) - **Comprehensive FHIR IG analytics** with resource breakdowns by version, authority, and realm (as running at http://packages2.fhir.org/packages)
* [Publisher](publisher/readme.md) - FHIR publishing services (coming)
Expand Down Expand Up @@ -43,6 +43,10 @@ There are 4 executable programs:

Unless you're developing, you only need the first two

FHIRsmith is open source - see below, and you're welcome to use it for any kind of use. Note,
though, that if you support FHIRsmith commercially as part of a managed service or product, you
are required to be a Commercial Partner of HL7 - see (link to be provided).

### Quick Start

* Install FHIRSmith (using docker, or an NPM release, or just get the code by git)
Expand Down
3 changes: 3 additions & 0 deletions security.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ A typical NGINX configuration would be:
limit_conn perip 50;
limit_conn perserver 500;
```
## SSL

This server doesn't provide SSL support - use an NGINX reverse proxy for that.

2 changes: 1 addition & 1 deletion stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class ServerStats {
if (this.taskMap.size == 0) {
return "";
}
let html = '<table class="grid">';
let html = '<table class="grid" >';
html += "<tr><th>Background Task</th><th>Status</th><th>Frequency</th><th>Last Seen</th></tr>";
for (let m of this.taskMap.keys()) {
let mm = this.taskMap.get(m);
Expand Down
12 changes: 8 additions & 4 deletions tx/cs/cs-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,11 +673,13 @@ class CodeSystemProvider {
/**
* register the concept maps that are implicitly defined as part of the code system
*
* @param {ConceptMap} map the map (this will have been returned from findImplicitConceptMap)
* @param {Coding} coding the coding to translate
* @param {String} target
* @returns {CodeTranslation[]} the list of translations
* @param {String} target the target code system
* @param {boolean} reverse - if the translation is being run backwards
* @returns {CodeTranslation[]} the list of translations, each CodeTranslation has map, code, system, version, display, and relationship
*/
async getTranslations(coding, target) { return null;}
async getTranslations(map, coding, target, reverse) { return null;}

// ==== Parameter checking methods =========
_ensureLanguages(param) {
Expand Down Expand Up @@ -853,7 +855,7 @@ class CodeSystemFactoryProvider {
}

/**
* see comemnts for registerSupplements()
* see comments for registerSupplements()
*
* @param {CodeSystem} supplement - the supplement to flesh out
* @returns void
Expand All @@ -865,6 +867,8 @@ class CodeSystemFactoryProvider {
/**
* build and return a known concept map from the URL, if there is one.
*
* the conceptmap is never visible to a user; if it has an implicitSource, then
* provider.getTranslations will be called when it's actually used
* @param url
* @param version
* @returns {ConceptMap}
Expand Down
8 changes: 5 additions & 3 deletions tx/cs/cs-omop.js
Original file line number Diff line number Diff line change
Expand Up @@ -660,8 +660,10 @@ class OMOPServices extends BaseCSServices {
}

// Translation support
async getTranslations(coding, target) {

async getTranslations(map, coding, target) {
if (map == null) {
return;
}

const vocabId = getVocabId(target);
if (vocabId === -1) {
Expand All @@ -680,7 +682,7 @@ class OMOPServices extends BaseCSServices {
reject(err);
} else {
const translations = rows.map(row => ({
uri: target,
system: target,
code: row.concept_code,
display: row.concept_name,
relationship: 'equivalent',
Expand Down
10 changes: 4 additions & 6 deletions tx/cs/cs-rxnorm.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,24 +395,24 @@ class RxNormServices extends CodeSystemProvider {
} else if (this.rels.includes(prop)) {
if (value.startsWith('CUI:')) {
const cui = value.substring(4);
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI1 FROM rxnrel WHERE REL = $rel AND RXCUI2 = $cui2)))`;
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE REL = $rel AND RXCUI1 = $cui2)))`;
params.rel = this.#sqlWrapString(prop);
params.cui2 = this.#sqlWrapString(cui);
} else if (value.startsWith('AUI:')) {
const aui = value.substring(4);
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI1 FROM rxnrel WHERE REL = $rel AND RXAUI2 = $aui2)))`;
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE REL = $rel AND RXAUI1 = $aui2)))`;
params.rel = this.#sqlWrapString(prop);
params.aui2 = this.#sqlWrapString(aui);
}
} else if (this.reltypes.includes(prop)) {
if (value.startsWith('CUI:')) {
const cui = value.substring(4);
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI1 FROM rxnrel WHERE RELA = $rela AND RXCUI2 = $cui2)))`;
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE RELA = $rela AND RXCUI1 = $cui2)))`;
params.rela = this.#sqlWrapString(prop);
params.cui2 = this.#sqlWrapString(cui);
} else if (value.startsWith('AUI:')) {
const aui = value.substring(4);
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI1 FROM rxnrel WHERE RELA = $rela AND RXAUI2 = $aui2)))`;
sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE RELA = $rela AND RXAUI1 = $aui2)))`;
params.rela = this.#sqlWrapString(prop);
params.aui2 = this.#sqlWrapString(aui);
}
Expand Down Expand Up @@ -498,8 +498,6 @@ class RxNormServices extends CodeSystemProvider {
}

async filterSize(filterContext, set) {


if (!set.executed) {
await this.#executeFilter(set);
}
Expand Down
114 changes: 113 additions & 1 deletion tx/cs/cs-snomed.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
const {DesignationUse} = require("../library/designations");
const {BaseCSServices} = require("./cs-base");
const {formatDateMMDDYYYY} = require("../../library/utilities");
const {ConceptMap} = require("../library/conceptmap");

// Context kinds matching Pascal enum
const SnomedProviderContextKind = {
Expand Down Expand Up @@ -1213,6 +1214,64 @@ class SnomedProvider extends BaseCSServices {
(cd.use.code === '900000000000013009' || cd.use.code === '900000000000003001');
}

async getTranslations(map, coding, target, reverse) {
if (!map || (target && target !== this.system()) || reverse) {
return [];
}
let ref = this.sct.concepts.findConcept(map.id);
if (!ref.found) {
return [];
}
let rref = this.sct.refSetIndex.getRefSetByConcept(ref.index);
if (rref == -1) {
return [];
}
let refSetRecord = this.sct.refSetIndex.getReferenceSet(rref);
let members = this.sct.refSetMembers.getMembers(refSetRecord.membersByRef);
let srcConcept = this.sct.concepts.findConcept(coding.code);
if (!srcConcept.found) {
return [];
}

let result = [];
let L = 0;
let H = members.length - 1;
while (L <= H) {
const I = Math.floor((L + H) / 2);
const ref = members[I].ref;
if (ref < srcConcept.index) {
L = I + 1;
} else if (ref > srcConcept.index) {
H = I - 1;
} else {
// Found — but scan left for first match in case of duplicates
let first = I;
while (first > 0 && members[first - 1].ref === srcConcept.index) {
first--;
}
// Process all matching members
for (let i = first; i < members.length && members[i].ref === srcConcept.index; i++) {
let values = this.sct.refs.getReferences(members[i].values);
if (values && values.length >= 1) {
let tgtId = String(this.sct.concepts.getConceptId(values[0]));
let ct = {
map: map.vurl,
code: tgtId,
system: this.system(),
version : this.version(),
display: await this.display(tgtId),
relationship: map.jsonObj.relationship
}
result.push(ct);
}
}
break;
}
}

return result;
}

}

/**
Expand Down Expand Up @@ -1339,7 +1398,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
return null;
}
let rref = this.snomedServices.refSetIndex.getRefSetByConcept(ref.index);
if (rref == 0) {
if (rref == -1) {
return null;
}
return {
Expand Down Expand Up @@ -1448,6 +1507,59 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {

return edition + ' ' + formatDateMMDDYYYY(match[2].substring(4, 6) + match[2].substring(6, 8) + match[2].substring(0, 4));
}

async findImplicitConceptMap(url, version) {
if (version && (version !== this.version())) {
return null;
}
if (!url || !url.startsWith(this.system()+"?fhir_cm=")) {
return null;
}
let id = url.substring(url.indexOf("=")+1);
if (['900000000000523009', '900000000000526001', '900000000000527005', '900000000000530003'].includes(id)) {
let name = '';
let relationship = '';
switch (id) {
case '900000000000523009':
name = 'POSSIBLY EQUIVALENT TO';
relationship = 'inexact';
break;
case '900000000000526001':
name = 'REPLACED BY';
relationship = 'equivalent';
break;
case '900000000000527005':
name = 'SAME AS';
relationship = 'equal';
break;
case '900000000000530003':
name = 'ALTERNATIVE';
relationship = 'inexact';
break;
}
let cm = {
resourceType: 'ConceptMap',
internalSource : this,
relationship: relationship,
id : id,
url: `${this.system}?fhir_cm=${id}`,
version: this.version(),
name: `SNOMED CT ${name} Concept Map`,
description: `The concept map implicitly defined by the ${name} Association Reference Set`,
copyright: 'This value set includes content from SNOMED CT, which is copyright © 2002+ International Health Terminology Standards Development Organisation (SNOMED International), and distributed by agreement between SNOMED International and HL7',
status: 'active',
sourceUri: `${this.system}?fhir_vs`,
targetUri: `${this.system}?fhir_vs`,
group: [{
source: 'http://snomed.info/sct',
target: 'http://snomed.info/sct'
}]
}
return new ConceptMap(cm);
} else {
return null;
}
}
}

function getEditionName(edition) {
Expand Down
26 changes: 26 additions & 0 deletions tx/library/conceptmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@ class ConceptMap extends CanonicalResource {
}
return result;
}

listTranslationsReverse(coding, targetScope, sourceSystem) {
let result = [];
let vurl = VersionUtilities.vurl(coding.system, coding.version);

let all = this.canonicalMatches(targetScope, this.targetScope);
for (const g of this.jsonObj.group || []) {
const targetOk = this.canonicalMatches(vurl, g.target);
const sourceOk = !sourceSystem || this.canonicalMatches(sourceSystem, g.source);
if (all || (sourceOk && targetOk)) {
for (const em of g.element || []) {
for (const tm of em.target || []) {
if (tm.code === coding.code) {
let match = {
group: g,
match: em,
target: tm
};
result.push(match);
}
}
}
}
}
return result;
}
/**
* Gets the source scope (R5) or source system (R3/R4)
* @returns {string|undefined} Source scope/system
Expand Down
7 changes: 4 additions & 3 deletions tx/library/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Extensions = {
}
},

checkNoModifiers(element, place, name) {
checkNoModifiers(element, place, name, resource) {
if (!element) {
return;
}
Expand All @@ -41,11 +41,12 @@ const Extensions = {
for (const extension of element.modifierExtension) {
urls.add(extension.url);
}
const resId = resource ? resource : "";
const urlList = [...urls].join('\', \'');
if (urls.size > 1) {
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of modifier extensions '+urlList);
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of modifier extensions '+urlList);
} else {
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of the modifier extension '+urlList);
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of the modifier extension '+urlList);
}
}
return true;
Expand Down
4 changes: 2 additions & 2 deletions tx/params.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ class TxParameters {
break;
}
case 'force-valueset-version': {
this.seeVersionRule().push(getValuePrimitive(p), true, 'override');
this.seeVersionRule(getValuePrimitive(p), true, 'override');
break;
}
case 'check-valueset-version': {
this.seeVersionRule().push(getValuePrimitive(p), true, 'check');
this.seeVersionRule(getValuePrimitive(p), true, 'check');
break;
}

Expand Down
5 changes: 4 additions & 1 deletion tx/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ class Provider {
for (let csp of this.codeSystemFactories.values()) {
if (!uris.has(csp.system())) {
uris.add(csp.system());
await csp.findImplicitConceptMap(url, version);
let cm = await csp.findImplicitConceptMap(url, version);
if (cm) {
return cm;
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tx/sct/structures.js
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,7 @@ class SnomedReferenceSetIndex {
}
}

return 0; // Not found
return -1; // Not found
}

count() {
Expand Down
Loading
Loading