fhirsmith 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +432 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -3
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +10 -0
- package/tx/README.md +52 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +92 -14
- package/tx/cs/cs-cs.js +64 -3
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +431 -0
- package/tx/cs/cs-loinc.js +296 -27
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +1210 -80
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +6 -1
- package/tx/params.js +8 -1
- package/tx/provider.js +24 -6
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/sct/structures.js +8 -2
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +3 -4
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +35 -8
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +138 -6
- package/utilities/make-sct-loinc-subset.js +250 -0
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -7,6 +7,7 @@ const { validateOptionalParameter, validateArrayParameter} = require("../../libr
|
|
|
7
7
|
const {BaseCSServices} = require("./cs-base");
|
|
8
8
|
const {sqlEscapeString} = require("../../xig/xig");
|
|
9
9
|
const regexUtilities = require('../../library/regex-utilities');
|
|
10
|
+
const {ConceptMap} = require("../library/conceptmap");
|
|
10
11
|
|
|
11
12
|
// Context kinds matching Pascal enum
|
|
12
13
|
const LoincProviderContextKind = {
|
|
@@ -16,6 +17,42 @@ const LoincProviderContextKind = {
|
|
|
16
17
|
ANSWER: 3 // lpckAnswer
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// LOINC's mappings to other code systems (the ExternalCodes / ExternalMappings tables, from
|
|
21
|
+
// PartRelatedCodeMapping, the IEEE mapping table and the answer lists). Each target system
|
|
22
|
+
// has an implicit ConceptMap http://loinc.org/cm/<key>, and its mappings are $lookup
|
|
23
|
+
// properties named <key>. Targets in systems not listed here are not used yet
|
|
24
|
+
const LOINC_MAPS = [
|
|
25
|
+
{ system: 'http://loinc.org', key: 'loinc' },
|
|
26
|
+
{ system: 'http://pubchem.ncbi.nlm.nih.gov', key: 'ncbi' },
|
|
27
|
+
{ system: 'http://snomed.info/sct', key: 'sct' },
|
|
28
|
+
{ system: 'http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets', key: 'hcpcs' },
|
|
29
|
+
{ system: 'http://www.genenames.org', key: 'gene-names' },
|
|
30
|
+
{ system: 'http://www.nlm.nih.gov/research/umls/rxnorm', key: 'rxnorm' },
|
|
31
|
+
{ system: 'https://www.ebi.ac.uk/chebi', key: 'chebi' },
|
|
32
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/clinvar', key: 'ncbi-clinvar' },
|
|
33
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/gene', key: 'ncbi-gene' },
|
|
34
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/taxonomy', key: 'ncbi-taxo' },
|
|
35
|
+
{ system: 'urn:iso:std:iso:11073:10101', key: 'mdc' },
|
|
36
|
+
{ system: 'urn:oid:2.16.840.1.113883.6.238', key: 'race' }
|
|
37
|
+
];
|
|
38
|
+
const LOINC_MAP_BASE = 'http://loinc.org/cm/';
|
|
39
|
+
|
|
40
|
+
// The mapping tables hold R4 equivalences, stated from the LOINC code to the external code.
|
|
41
|
+
// Translations report R5 relationships, from the code being translated to the one found
|
|
42
|
+
function mapRelationship(equivalence, fromLoinc) {
|
|
43
|
+
switch (equivalence) {
|
|
44
|
+
case 'equivalent':
|
|
45
|
+
case 'equal':
|
|
46
|
+
return 'equivalent';
|
|
47
|
+
case 'narrower': // the external code is narrower than the LOINC code
|
|
48
|
+
return fromLoinc ? 'source-is-broader-than-target' : 'source-is-narrower-than-target';
|
|
49
|
+
case 'wider':
|
|
50
|
+
return fromLoinc ? 'source-is-narrower-than-target' : 'source-is-broader-than-target';
|
|
51
|
+
default:
|
|
52
|
+
return 'related-to';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
const classTypes = {
|
|
20
57
|
'1': 'Laboratory class',
|
|
21
58
|
'2': 'Clinical class',
|
|
@@ -127,6 +164,7 @@ class LoincServices extends BaseCSServices {
|
|
|
127
164
|
this.propertyList = sharedData.propertyList;
|
|
128
165
|
this.statusKeys = sharedData.statusKeys;
|
|
129
166
|
this.statusCodes = sharedData.statusCodes;
|
|
167
|
+
this.hasMappings = sharedData.hasMappings;
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
close() {
|
|
@@ -149,6 +187,12 @@ class LoincServices extends BaseCSServices {
|
|
|
149
187
|
return 'LOINC';
|
|
150
188
|
}
|
|
151
189
|
|
|
190
|
+
/** $lookup's display name: the code system's title. */
|
|
191
|
+
// eslint-disable-next-line no-unused-vars
|
|
192
|
+
lookupName(languages) {
|
|
193
|
+
return 'Logical Observation Identifier Names and Codes (LOINC)';
|
|
194
|
+
}
|
|
195
|
+
|
|
152
196
|
description() {
|
|
153
197
|
return 'LOINC';
|
|
154
198
|
}
|
|
@@ -313,10 +357,114 @@ class LoincServices extends BaseCSServices {
|
|
|
313
357
|
this.#addRelationshipProperties(ctxt, props, params),
|
|
314
358
|
this.#addConceptProperties(ctxt, props,params),
|
|
315
359
|
this.#addStatusProperty(ctxt, props,params),
|
|
316
|
-
this.#addRelatedNames(ctxt, props,params)
|
|
360
|
+
this.#addRelatedNames(ctxt, props,params),
|
|
361
|
+
this.#addMappingProperties(ctxt, props, params)
|
|
317
362
|
]);
|
|
318
363
|
}
|
|
319
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The code's mappings to other code systems, as Coding-valued properties named for the
|
|
367
|
+
* map (sct, chebi, mdc...), each with its equivalence as a subproperty. Returned by
|
|
368
|
+
* default, and when asked for by name
|
|
369
|
+
*/
|
|
370
|
+
async #addMappingProperties(ctxt, props, params) {
|
|
371
|
+
if (!this.hasMappings) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const rows = await this.#dbAll(`
|
|
375
|
+
SELECT ExternalCodes.System, ExternalCodes.Version, ExternalCodes.Code, ExternalCodes.Display, ExternalMappings.Equivalence
|
|
376
|
+
FROM ExternalMappings, ExternalCodes
|
|
377
|
+
WHERE ExternalMappings.CodeKey = ?
|
|
378
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
379
|
+
ORDER BY ExternalCodes.System, ExternalCodes.Code`, [ctxt.key]);
|
|
380
|
+
for (const row of rows) {
|
|
381
|
+
const map = LOINC_MAPS.find(m => m.system === row.System);
|
|
382
|
+
if (map && this._hasProp(props, map.key, true)) {
|
|
383
|
+
const coding = { system: row.System };
|
|
384
|
+
if (row.Version) {
|
|
385
|
+
coding.version = row.Version;
|
|
386
|
+
}
|
|
387
|
+
coding.code = row.Code;
|
|
388
|
+
if (row.Display) {
|
|
389
|
+
coding.display = row.Display;
|
|
390
|
+
}
|
|
391
|
+
params.push({
|
|
392
|
+
name: 'property',
|
|
393
|
+
part: [
|
|
394
|
+
{ name: 'code', valueCode: map.key },
|
|
395
|
+
{ name: 'value', valueCoding: coding },
|
|
396
|
+
{ name: 'subproperty', part: [{ name: 'code', valueCode: 'equivalence' }, { name: 'value', valueCode: row.Equivalence }] }
|
|
397
|
+
]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#dbAll(sql, args) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
this.db.all(sql, args, (err, rows) => err ? reject(err) : resolve(rows));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Translate using one of LOINC's implicit maps (http://loinc.org/cm/<key>). The map works
|
|
411
|
+
* in both directions, decided by the system of the code being translated: a LOINC code to
|
|
412
|
+
* the codes it maps to in the map's system, or a code in that system to the LOINC codes
|
|
413
|
+
* that map to it (for the LOINC to LOINC map, always the first). The version of a non-LOINC
|
|
414
|
+
* code is not used: the mappings name one version of each external system
|
|
415
|
+
*/
|
|
416
|
+
async getTranslations(map, coding, target, reverse) { // eslint-disable-line no-unused-vars
|
|
417
|
+
const lm = map && map.jsonObj.loincMap;
|
|
418
|
+
if (!lm || !this.hasMappings) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const targetSystem = target ? target.split('|')[0] : null;
|
|
422
|
+
const result = [];
|
|
423
|
+
if (coding.system === this.system() && (!targetSystem || targetSystem === lm.system)) {
|
|
424
|
+
const ctxt = this.codes.get(coding.code);
|
|
425
|
+
if (!ctxt) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const rows = await this.#dbAll(`
|
|
429
|
+
SELECT ExternalCodes.Version, ExternalCodes.Code, ExternalCodes.Display, ExternalMappings.Equivalence
|
|
430
|
+
FROM ExternalMappings, ExternalCodes
|
|
431
|
+
WHERE ExternalMappings.CodeKey = ?
|
|
432
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
433
|
+
AND ExternalCodes.System = ?
|
|
434
|
+
ORDER BY ExternalCodes.Code`, [ctxt.key, lm.system]);
|
|
435
|
+
for (const row of rows) {
|
|
436
|
+
const t = { map: map.vurl, system: lm.system, code: row.Code, relationship: mapRelationship(row.Equivalence, true) };
|
|
437
|
+
if (row.Version) {
|
|
438
|
+
t.version = row.Version;
|
|
439
|
+
}
|
|
440
|
+
if (row.Display) {
|
|
441
|
+
t.display = row.Display;
|
|
442
|
+
}
|
|
443
|
+
result.push(t);
|
|
444
|
+
}
|
|
445
|
+
} else if (coding.system === lm.system && (!targetSystem || targetSystem === this.system())) {
|
|
446
|
+
const rows = await this.#dbAll(`
|
|
447
|
+
SELECT Codes.Code, Codes.Description, ExternalMappings.Equivalence
|
|
448
|
+
FROM ExternalCodes, ExternalMappings, Codes
|
|
449
|
+
WHERE ExternalCodes.System = ?
|
|
450
|
+
AND ExternalCodes.Code = ?
|
|
451
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
452
|
+
AND Codes.CodeKey = ExternalMappings.CodeKey
|
|
453
|
+
ORDER BY Codes.Code`, [lm.system, coding.code]);
|
|
454
|
+
for (const row of rows) {
|
|
455
|
+
result.push({
|
|
456
|
+
map: map.vurl,
|
|
457
|
+
system: this.system(),
|
|
458
|
+
version: this.version(),
|
|
459
|
+
code: row.Code,
|
|
460
|
+
display: row.Description,
|
|
461
|
+
relationship: mapRelationship(row.Equivalence, false)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
|
|
320
468
|
#getDesignationUse(kind) {
|
|
321
469
|
switch (kind) {
|
|
322
470
|
case LoincProviderContextKind.CODE:
|
|
@@ -607,12 +755,18 @@ class LoincServices extends BaseCSServices {
|
|
|
607
755
|
// Filter support
|
|
608
756
|
async doesFilter(prop, op, value) {
|
|
609
757
|
// Relationship filters
|
|
610
|
-
if (this.relationships.has(prop) && ['=', 'in', '
|
|
758
|
+
if (this.relationships.has(prop) && ['=', 'in', 'regex'].includes(op)) {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
if (this.relationships.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
|
|
611
762
|
return true;
|
|
612
763
|
}
|
|
613
764
|
|
|
614
765
|
// Property filters
|
|
615
|
-
if (this.propertyList.has(prop) && ['=', 'in', '
|
|
766
|
+
if (this.propertyList.has(prop) && ['=', 'in', 'regex'].includes(op)) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
if (this.propertyList.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
|
|
616
770
|
return true;
|
|
617
771
|
}
|
|
618
772
|
|
|
@@ -748,26 +902,26 @@ class LoincServices extends BaseCSServices {
|
|
|
748
902
|
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
|
|
749
903
|
AND SourceKey = `;
|
|
750
904
|
}
|
|
751
|
-
// Relationship 'exists' filter
|
|
905
|
+
// Relationship 'exists' filter: value is true or false (doesFilter rejects anything else).
|
|
906
|
+
// true = codes that have at least one relationship of this type; false = every other code
|
|
907
|
+
// in the code system (Parts, AnswerLists and Answers included - combine with e.g.
|
|
908
|
+
// CLASSTYPE exists true to restrict to LOINC terms)
|
|
752
909
|
else if (this.relationships.has(prop) && op === 'exists') {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
756
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
757
|
-
ORDER BY SourceKey ASC`;
|
|
758
|
-
lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
|
|
759
|
-
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
760
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
761
|
-
AND SourceKey = `;
|
|
762
|
-
} else {
|
|
910
|
+
const rk = this.relationships.get(prop);
|
|
911
|
+
if (value === 'true') {
|
|
763
912
|
sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
|
|
764
|
-
WHERE RelationshipTypeKey = ${
|
|
765
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
913
|
+
WHERE RelationshipTypeKey = ${rk}
|
|
766
914
|
ORDER BY SourceKey ASC`;
|
|
767
|
-
lsql = `SELECT COUNT(
|
|
768
|
-
WHERE RelationshipTypeKey = ${
|
|
769
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
915
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
916
|
+
WHERE RelationshipTypeKey = ${rk}
|
|
770
917
|
AND SourceKey = `;
|
|
918
|
+
} else if (value === 'false') {
|
|
919
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
920
|
+
WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
|
|
921
|
+
ORDER BY CodeKey ASC`;
|
|
922
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
923
|
+
WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
|
|
924
|
+
AND CodeKey = `;
|
|
771
925
|
}
|
|
772
926
|
}
|
|
773
927
|
// Relationship regex filter
|
|
@@ -820,14 +974,24 @@ class LoincServices extends BaseCSServices {
|
|
|
820
974
|
AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
|
|
821
975
|
AND CodeKey = `;
|
|
822
976
|
}
|
|
823
|
-
// Property 'exists' filter
|
|
977
|
+
// Property 'exists' filter: value is true or false, as for relationships above
|
|
824
978
|
else if (this.propertyList.has(prop) && op === 'exists') {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
979
|
+
const pk = this.propertyList.get(prop);
|
|
980
|
+
if (value === 'true') {
|
|
981
|
+
sql = `SELECT DISTINCT CodeKey as Key FROM Properties
|
|
982
|
+
WHERE Properties.PropertyTypeKey = ${pk}
|
|
983
|
+
ORDER BY CodeKey ASC`;
|
|
984
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties
|
|
985
|
+
WHERE Properties.PropertyTypeKey = ${pk}
|
|
986
|
+
AND CodeKey = `;
|
|
987
|
+
} else if (value === 'false') {
|
|
988
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
989
|
+
WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
|
|
990
|
+
ORDER BY CodeKey ASC`;
|
|
991
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
992
|
+
WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
|
|
993
|
+
AND CodeKey = `;
|
|
994
|
+
}
|
|
831
995
|
}
|
|
832
996
|
// Property regex filter
|
|
833
997
|
else if (this.propertyList.has(prop) && op === 'regex') {
|
|
@@ -1156,6 +1320,9 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1156
1320
|
propertyList: new Map(),
|
|
1157
1321
|
statusKeys: new Map(),
|
|
1158
1322
|
statusCodes: new Map(),
|
|
1323
|
+
hasMappings: false,
|
|
1324
|
+
mappedCodes: new Map(), // LOINC code -> Set of the systems it maps to
|
|
1325
|
+
mappedTargets: new Set(), // system|code of every external code something maps to
|
|
1159
1326
|
_version: '',
|
|
1160
1327
|
root: '',
|
|
1161
1328
|
firstCodeKey: 0
|
|
@@ -1168,11 +1335,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1168
1335
|
this.#loadStatusCodes(db),
|
|
1169
1336
|
this.#loadRelationshipTypes(db),
|
|
1170
1337
|
this.#loadPropertyTypes(db),
|
|
1171
|
-
this.#loadConfig(db)
|
|
1338
|
+
this.#loadConfig(db),
|
|
1339
|
+
this.#loadHasMappings(db)
|
|
1172
1340
|
]);
|
|
1173
1341
|
|
|
1174
1342
|
// Load codes (largest operation)
|
|
1175
1343
|
await this.#loadCodes(db);
|
|
1344
|
+
await this.#loadMappingIndex(db);
|
|
1176
1345
|
|
|
1177
1346
|
// Load dependent data in parallel
|
|
1178
1347
|
await Promise.all([
|
|
@@ -1398,6 +1567,106 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1398
1567
|
return this._sharedData?._version || 'unknown';
|
|
1399
1568
|
}
|
|
1400
1569
|
|
|
1570
|
+
// the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
|
|
1571
|
+
async #loadHasMappings(db) {
|
|
1572
|
+
return new Promise((resolve, reject) => {
|
|
1573
|
+
db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", (err, row) => {
|
|
1574
|
+
if (err) {
|
|
1575
|
+
reject(err);
|
|
1576
|
+
} else {
|
|
1577
|
+
this._sharedData.hasMappings = !!row;
|
|
1578
|
+
resolve();
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// which codes have mappings, so that a map is only offered for a code it says something about
|
|
1585
|
+
async #loadMappingIndex(db) {
|
|
1586
|
+
if (!this._sharedData.hasMappings) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
return new Promise((resolve, reject) => {
|
|
1590
|
+
db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
|
|
1591
|
+
FROM ExternalMappings, ExternalCodes, Codes
|
|
1592
|
+
WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
1593
|
+
AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
|
|
1594
|
+
if (err) {
|
|
1595
|
+
reject(err);
|
|
1596
|
+
} else {
|
|
1597
|
+
for (const row of rows) {
|
|
1598
|
+
let systems = this._sharedData.mappedCodes.get(row.LoincCode);
|
|
1599
|
+
if (!systems) {
|
|
1600
|
+
systems = new Set();
|
|
1601
|
+
this._sharedData.mappedCodes.set(row.LoincCode, systems);
|
|
1602
|
+
}
|
|
1603
|
+
systems.add(row.System);
|
|
1604
|
+
this._sharedData.mappedTargets.add(row.System + '|' + row.Code);
|
|
1605
|
+
}
|
|
1606
|
+
resolve();
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* LOINC's implicit maps (see LOINC_MAPS): from LOINC to a system it maps to, or from that
|
|
1614
|
+
* system to LOINC. When the code being translated is known, the map is only offered if it
|
|
1615
|
+
* has a mapping for it - so a translation between LOINC and SNOMED CT that the SNOMED CT
|
|
1616
|
+
* edition answers (the LOINC Extension) does not also report a LOINC map that says nothing.
|
|
1617
|
+
* The code may be on either side (under reverse it is the target concept)
|
|
1618
|
+
*/
|
|
1619
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest, code = null) { // eslint-disable-line no-unused-vars
|
|
1620
|
+
await this.#ensureLoaded();
|
|
1621
|
+
if (!dest || !this._sharedData.hasMappings) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const destSystem = dest.split('|')[0];
|
|
1625
|
+
let other = null;
|
|
1626
|
+
if (source === this.system()) {
|
|
1627
|
+
other = destSystem;
|
|
1628
|
+
} else if (destSystem === this.system()) {
|
|
1629
|
+
other = source;
|
|
1630
|
+
}
|
|
1631
|
+
const lm = LOINC_MAPS.find(m => m.system === other);
|
|
1632
|
+
if (!lm || conceptMaps.some(m => m.url === LOINC_MAP_BASE + lm.key)) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (code && !(this._sharedData.mappedCodes.get(code)?.has(lm.system)) && !this._sharedData.mappedTargets.has(lm.system + '|' + code)) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
conceptMaps.push(this.#loincMap(lm));
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async findImplicitConceptMap(url, version) {
|
|
1642
|
+
if (!url || !url.startsWith(LOINC_MAP_BASE)) {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
await this.#ensureLoaded();
|
|
1646
|
+
if ((version && version !== this.version()) || !this._sharedData.hasMappings) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
const lm = LOINC_MAPS.find(m => LOINC_MAP_BASE + m.key === url);
|
|
1650
|
+
return lm ? this.#loincMap(lm) : null;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
#loincMap(lm) {
|
|
1654
|
+
const groups = [{ source: this.system(), target: lm.system }];
|
|
1655
|
+
if (lm.system !== this.system()) {
|
|
1656
|
+
groups.push({ source: lm.system, target: this.system() });
|
|
1657
|
+
}
|
|
1658
|
+
return new ConceptMap({
|
|
1659
|
+
resourceType: 'ConceptMap',
|
|
1660
|
+
internalSource: this,
|
|
1661
|
+
loincMap: lm,
|
|
1662
|
+
url: LOINC_MAP_BASE + lm.key,
|
|
1663
|
+
name: `LOINC ${lm.key} Concept Map`,
|
|
1664
|
+
description: `The mappings between LOINC ${this.version()} and ${lm.system}, as published by LOINC`,
|
|
1665
|
+
status: 'active',
|
|
1666
|
+
group: groups
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1401
1670
|
async build(opContext, supplements) {
|
|
1402
1671
|
await this.#ensureLoaded();
|
|
1403
1672
|
this.recordUse();
|
package/tx/cs/cs-omop.js
CHANGED
|
@@ -151,7 +151,13 @@ class OMOPServices extends BaseCSServices {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
name() {
|
|
154
|
-
return
|
|
154
|
+
return 'OMOP_Concepts';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** $lookup's display name: the code system's title. */
|
|
158
|
+
// eslint-disable-next-line no-unused-vars
|
|
159
|
+
lookupName(languages) {
|
|
160
|
+
return 'OMOP Unified Vocabularies';
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
description() {
|
|
@@ -952,7 +958,8 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
|
952
958
|
* @param version
|
|
953
959
|
* @returns {ConceptMap}
|
|
954
960
|
*/
|
|
955
|
-
|
|
961
|
+
// eslint-disable-next-line no-unused-vars
|
|
962
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
|
|
956
963
|
if (source == 'https://fhir-terminology.ohdsi.org') {
|
|
957
964
|
let key = this.#getVocabId(dest);
|
|
958
965
|
if (key) {
|
|
@@ -1007,7 +1014,7 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1007
1014
|
|
|
1008
1015
|
|
|
1009
1016
|
name() {
|
|
1010
|
-
return
|
|
1017
|
+
return 'OMOP_Concepts';
|
|
1011
1018
|
}
|
|
1012
1019
|
|
|
1013
1020
|
id() {
|