fhirsmith 0.18.2 → 1.0.0
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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -2,12 +2,13 @@ const sqlite3 = require('sqlite3').verbose();
|
|
|
2
2
|
const assert = require('assert');
|
|
3
3
|
const { CodeSystem } = require('../library/codesystem');
|
|
4
4
|
const { Language, Languages} = require('../../library/languages');
|
|
5
|
-
const { CodeSystemFactoryProvider} = require('./cs-api');
|
|
5
|
+
const { CodeSystemFactoryProvider, CodeSystemProvider } = require('./cs-api');
|
|
6
6
|
const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
|
|
7
7
|
const {BaseCSServices} = require("./cs-base");
|
|
8
|
-
const {sqlEscapeString} = require("../../
|
|
8
|
+
const {sqlEscapeString} = require("../../library/sqlite-utilities");
|
|
9
9
|
const regexUtilities = require('../../library/regex-utilities');
|
|
10
10
|
const {ConceptMap} = require("../library/conceptmap");
|
|
11
|
+
const { guarded } = require('../library/sqlite-utilities');
|
|
11
12
|
|
|
12
13
|
// Context kinds matching Pascal enum
|
|
13
14
|
const LoincProviderContextKind = {
|
|
@@ -53,13 +54,16 @@ function mapRelationship(equivalence, fromLoinc) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// CLASSTYPE: the code and the description the importer stores (see descClassType in
|
|
58
|
+
// import-loinc.module.js - 'Claims attachment', singular), and back
|
|
56
59
|
const classTypes = {
|
|
57
60
|
'1': 'Laboratory class',
|
|
58
61
|
'2': 'Clinical class',
|
|
59
|
-
'3': 'Claims
|
|
62
|
+
'3': 'Claims attachment',
|
|
60
63
|
'4': 'Surveys',
|
|
61
64
|
'Laboratory class' : '1',
|
|
62
65
|
'Clinical class' : '2',
|
|
66
|
+
'Claims attachment' : '3',
|
|
63
67
|
'Claims attachments' : '3',
|
|
64
68
|
'Surveys' : '4'
|
|
65
69
|
};
|
|
@@ -124,6 +128,9 @@ class LoincFilterHolder {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
hasKey(key) {
|
|
131
|
+
if (this.keySet) {
|
|
132
|
+
return this.keySet.has(key);
|
|
133
|
+
}
|
|
127
134
|
// Binary search since keys should be sorted
|
|
128
135
|
let l = 0;
|
|
129
136
|
let r = this.keys.length - 1;
|
|
@@ -142,8 +149,9 @@ class LoincFilterHolder {
|
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
class LoincPrep {
|
|
145
|
-
constructor() {
|
|
152
|
+
constructor(languages = null) {
|
|
146
153
|
this.filters = [];
|
|
154
|
+
this.languages = languages; // what the request asked for (a text filter searches one language)
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
@@ -168,10 +176,7 @@ class LoincServices extends BaseCSServices {
|
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
close() {
|
|
171
|
-
|
|
172
|
-
this.db.close();
|
|
173
|
-
this.db = null;
|
|
174
|
-
}
|
|
179
|
+
this.closeDb();
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
// Metadata methods
|
|
@@ -336,6 +341,14 @@ class LoincServices extends BaseCSServices {
|
|
|
336
341
|
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The concept's properties for $expand: built by extendLookup, so that an expansion reports
|
|
346
|
+
* the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup)
|
|
347
|
+
*/
|
|
348
|
+
async properties(context, names) {
|
|
349
|
+
return this._propertiesFromLookup(context, names);
|
|
350
|
+
}
|
|
351
|
+
|
|
339
352
|
async extendLookup(ctxt, props, params) {
|
|
340
353
|
validateArrayParameter(props, 'props', String);
|
|
341
354
|
validateArrayParameter(params, 'params', Object);
|
|
@@ -478,15 +491,18 @@ class LoincServices extends BaseCSServices {
|
|
|
478
491
|
|
|
479
492
|
async #addRelationshipProperties(ctxt, props, params) {
|
|
480
493
|
return new Promise((resolve, reject) => {
|
|
494
|
+
// DISTINCT: databases imported before the importer de-duplicated relationships have the
|
|
495
|
+
// same row more than once - a part listed twice under the same parent in
|
|
496
|
+
// ComponentHierarchyBySystem (on two paths) has two parent rows
|
|
481
497
|
const sql = `
|
|
482
|
-
SELECT RelationshipTypes.Description as Relationship, Codes.Code, Codes.Description as Value
|
|
498
|
+
SELECT DISTINCT RelationshipTypes.Description as Relationship, Codes.Code, Codes.Description as Value
|
|
483
499
|
FROM Relationships, RelationshipTypes, Codes
|
|
484
500
|
WHERE Relationships.SourceKey = ?
|
|
485
501
|
AND Relationships.RelationshipTypeKey = RelationshipTypes.RelationshipTypeKey
|
|
486
502
|
AND Relationships.TargetKey = Codes.CodeKey
|
|
487
503
|
`;
|
|
488
504
|
|
|
489
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
505
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
490
506
|
if (err) {
|
|
491
507
|
reject(err);
|
|
492
508
|
} else {
|
|
@@ -504,7 +520,7 @@ class LoincServices extends BaseCSServices {
|
|
|
504
520
|
}
|
|
505
521
|
resolve();
|
|
506
522
|
}
|
|
507
|
-
});
|
|
523
|
+
}));
|
|
508
524
|
});
|
|
509
525
|
}
|
|
510
526
|
|
|
@@ -518,14 +534,15 @@ class LoincServices extends BaseCSServices {
|
|
|
518
534
|
AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
|
|
519
535
|
`;
|
|
520
536
|
|
|
521
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
537
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
522
538
|
if (err) {
|
|
523
539
|
reject(err);
|
|
524
540
|
} else {
|
|
525
541
|
for (const row of rows) {
|
|
526
542
|
if (this._hasProp(props, row.Description, true)) {
|
|
527
543
|
if (row.Description == 'CLASSTYPE') {
|
|
528
|
-
|
|
544
|
+
// the code (1-4), with its description; a description with no known code is the value
|
|
545
|
+
this._addStringProperty(params, 'property', row.Description, classTypes[row.Value] || row.Value)
|
|
529
546
|
.part.push({name: 'description', valueString: row.Value});
|
|
530
547
|
} else {
|
|
531
548
|
this._addStringProperty(params, 'property', row.Description, row.Value);
|
|
@@ -534,7 +551,7 @@ class LoincServices extends BaseCSServices {
|
|
|
534
551
|
}
|
|
535
552
|
resolve();
|
|
536
553
|
}
|
|
537
|
-
});
|
|
554
|
+
}));
|
|
538
555
|
});
|
|
539
556
|
}
|
|
540
557
|
|
|
@@ -542,7 +559,7 @@ class LoincServices extends BaseCSServices {
|
|
|
542
559
|
return new Promise((resolve, reject) => {
|
|
543
560
|
const sql = 'SELECT StatusKey FROM Codes WHERE CodeKey = ? AND StatusKey != 0';
|
|
544
561
|
|
|
545
|
-
this.db.get(sql, [ctxt.key], (err, row) => {
|
|
562
|
+
this.db.get(sql, [ctxt.key], guarded(reject, (err, row) => {
|
|
546
563
|
if (err) {
|
|
547
564
|
reject(err);
|
|
548
565
|
} else if (row) {
|
|
@@ -556,7 +573,7 @@ class LoincServices extends BaseCSServices {
|
|
|
556
573
|
} else {
|
|
557
574
|
resolve();
|
|
558
575
|
}
|
|
559
|
-
});
|
|
576
|
+
}));
|
|
560
577
|
});
|
|
561
578
|
}
|
|
562
579
|
|
|
@@ -584,7 +601,7 @@ class LoincServices extends BaseCSServices {
|
|
|
584
601
|
AND Descriptions.LanguageKey = Languages.LanguageKey
|
|
585
602
|
`;
|
|
586
603
|
|
|
587
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
604
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
588
605
|
if (err) {
|
|
589
606
|
reject(err);
|
|
590
607
|
} else {
|
|
@@ -593,7 +610,7 @@ class LoincServices extends BaseCSServices {
|
|
|
593
610
|
}
|
|
594
611
|
resolve();
|
|
595
612
|
}
|
|
596
|
-
});
|
|
613
|
+
}));
|
|
597
614
|
});
|
|
598
615
|
}
|
|
599
616
|
|
|
@@ -611,7 +628,7 @@ class LoincServices extends BaseCSServices {
|
|
|
611
628
|
ORDER BY DescriptionTypeKey
|
|
612
629
|
`;
|
|
613
630
|
|
|
614
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
631
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
615
632
|
if (err) {
|
|
616
633
|
reject(err);
|
|
617
634
|
} else {
|
|
@@ -624,7 +641,7 @@ class LoincServices extends BaseCSServices {
|
|
|
624
641
|
|
|
625
642
|
resolve(displays);
|
|
626
643
|
}
|
|
627
|
-
});
|
|
644
|
+
}));
|
|
628
645
|
});
|
|
629
646
|
}
|
|
630
647
|
|
|
@@ -658,7 +675,7 @@ class LoincServices extends BaseCSServices {
|
|
|
658
675
|
AND Descriptions.LanguageKey = Languages.LanguageKey
|
|
659
676
|
`;
|
|
660
677
|
|
|
661
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
678
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
662
679
|
if (err) {
|
|
663
680
|
reject(err);
|
|
664
681
|
} else {
|
|
@@ -669,7 +686,7 @@ class LoincServices extends BaseCSServices {
|
|
|
669
686
|
}
|
|
670
687
|
resolve(results);
|
|
671
688
|
}
|
|
672
|
-
});
|
|
689
|
+
}));
|
|
673
690
|
});
|
|
674
691
|
}
|
|
675
692
|
|
|
@@ -683,7 +700,7 @@ class LoincServices extends BaseCSServices {
|
|
|
683
700
|
AND Descriptions.LanguageKey = Languages.LanguageKey
|
|
684
701
|
`;
|
|
685
702
|
|
|
686
|
-
this.db.all(sql, [ctxt.key], (err, rows) => {
|
|
703
|
+
this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
|
|
687
704
|
if (err) {
|
|
688
705
|
reject(err);
|
|
689
706
|
} else {
|
|
@@ -693,7 +710,7 @@ class LoincServices extends BaseCSServices {
|
|
|
693
710
|
}
|
|
694
711
|
resolve(results);
|
|
695
712
|
}
|
|
696
|
-
});
|
|
713
|
+
}));
|
|
697
714
|
});
|
|
698
715
|
}
|
|
699
716
|
|
|
@@ -782,11 +799,17 @@ class LoincServices extends BaseCSServices {
|
|
|
782
799
|
return true;
|
|
783
800
|
}
|
|
784
801
|
|
|
785
|
-
// LIST filter
|
|
802
|
+
// LIST filter: tx.fhir.org's own name for answers-for = [answer list], supported for
|
|
803
|
+
// backwards compatibility only (the defined filters are answers-for and answer-list, UP-645)
|
|
786
804
|
if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
|
|
787
805
|
return true;
|
|
788
806
|
}
|
|
789
807
|
|
|
808
|
+
// answer-list filter: the answer lists (LL) that an answer (LA) is in (UP-645)
|
|
809
|
+
if (prop === 'answer-list' && op === '=') {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
|
|
790
813
|
// CLASSSTYPE filter
|
|
791
814
|
if (prop === 'CLASSTYPE' && op === '=' && ["1", "2", "3", "4"].includes(value)) {
|
|
792
815
|
return true;
|
|
@@ -802,6 +825,13 @@ class LoincServices extends BaseCSServices {
|
|
|
802
825
|
return true;
|
|
803
826
|
}
|
|
804
827
|
|
|
828
|
+
// LOINC's own hierarchy filter: ancestor = part (or in parts), the codes the part subsumes
|
|
829
|
+
// in the multi-axial hierarchy. LOINC's implicit value sets (http://loinc.org/vs/[partcode])
|
|
830
|
+
// are defined with it
|
|
831
|
+
if (prop === 'ancestor' && ['=', 'in'].includes(op)) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
|
|
805
835
|
// code filters (VSAC workaround)
|
|
806
836
|
if (prop === 'code' && ['is-a', 'descendent-of', '='].includes(op)) {
|
|
807
837
|
return true;
|
|
@@ -815,8 +845,8 @@ class LoincServices extends BaseCSServices {
|
|
|
815
845
|
return false;
|
|
816
846
|
}
|
|
817
847
|
|
|
818
|
-
async getPrepContext(iterate) {
|
|
819
|
-
return new LoincPrep(
|
|
848
|
+
async getPrepContext(iterate, languages = null) {
|
|
849
|
+
return new LoincPrep(languages);
|
|
820
850
|
}
|
|
821
851
|
|
|
822
852
|
async filter(filterContext, forIteration, prop, op, value) {
|
|
@@ -825,10 +855,49 @@ class LoincServices extends BaseCSServices {
|
|
|
825
855
|
filterContext.filters.push(filter);
|
|
826
856
|
}
|
|
827
857
|
|
|
858
|
+
/**
|
|
859
|
+
* A text filter matches the codes with a name in the search language (see searchLanguage:
|
|
860
|
+
* the request's language if LOINC has it, else English) that contains the text, and the
|
|
861
|
+
* codes that a supplement in use gives a matching designation in that language. In English
|
|
862
|
+
* the name is the display (scanning every English description - short names, consumer
|
|
863
|
+
* names - takes 2+ seconds, against 0.2 for the displays); in another language it is any
|
|
864
|
+
* description in it but RELATEDNAMES2, which is a list of keywords (a property), not a name.
|
|
865
|
+
* LOINC does not rate its matches, so sorted or not they come in display order
|
|
866
|
+
*/
|
|
828
867
|
async searchFilter(filterContext, filterText, sort) { // eslint-disable-line no-unused-vars
|
|
829
868
|
const filter = new LoincFilterHolder();
|
|
830
|
-
|
|
831
|
-
|
|
869
|
+
const lang = this.searchLanguage(filterContext.languages);
|
|
870
|
+
const english = CodeSystemProvider.inSearchLanguage(null, lang);
|
|
871
|
+
const langKeys = [];
|
|
872
|
+
for (const [code, key] of this.langs) {
|
|
873
|
+
if (!english && CodeSystemProvider.inSearchLanguage(code, lang)) {
|
|
874
|
+
langKeys.push(key);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const text = sqlEscapeString(filterText.filter || '');
|
|
878
|
+
const conds = [];
|
|
879
|
+
if (english) {
|
|
880
|
+
conds.push(`Codes.Description like '%${text}%'`); // the display, which is English
|
|
881
|
+
}
|
|
882
|
+
if (langKeys.length > 0) {
|
|
883
|
+
conds.push(`Codes.CodeKey IN (SELECT CodeKey FROM Descriptions WHERE LanguageKey IN (${langKeys.join(',')})
|
|
884
|
+
AND DescriptionTypeKey != 4 AND Value like '%${text}%')`);
|
|
885
|
+
}
|
|
886
|
+
if (conds.length > 0) {
|
|
887
|
+
await this.#executeSQL(`SELECT Codes.CodeKey as Key FROM Codes WHERE ${conds.join(' OR ')} ORDER BY Codes.Description ASC`, filter);
|
|
888
|
+
} else {
|
|
889
|
+
filter.keys = [];
|
|
890
|
+
}
|
|
891
|
+
const have = new Set(filter.keys);
|
|
892
|
+
for (const code of this._supplementSearchCodes(filterText, lang)) {
|
|
893
|
+
const ctxt = this.codes.get(code);
|
|
894
|
+
if (ctxt && !have.has(ctxt.key)) {
|
|
895
|
+
filter.keys.push(ctxt.key);
|
|
896
|
+
have.add(ctxt.key);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
filter.lsql = 'text'; // checked by key (filterLocate needs it set)
|
|
900
|
+
filter.keySet = new Set(filter.keys); // in display order, not key order, so hasKey can't binary search
|
|
832
901
|
filterContext.filters.push(filter);
|
|
833
902
|
}
|
|
834
903
|
|
|
@@ -847,6 +916,17 @@ class LoincServices extends BaseCSServices {
|
|
|
847
916
|
AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
848
917
|
AND TargetKey = `;
|
|
849
918
|
}
|
|
919
|
+
// answer-list filter: the answer lists (LL) the answer (LA) is in - the inverse of answers-for
|
|
920
|
+
else if (prop === 'answer-list' && op === '=') {
|
|
921
|
+
sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
|
|
922
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
923
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
924
|
+
ORDER BY SourceKey ASC`;
|
|
925
|
+
lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
|
|
926
|
+
WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
|
|
927
|
+
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
928
|
+
AND SourceKey = `;
|
|
929
|
+
}
|
|
850
930
|
// answers-for filter
|
|
851
931
|
else if (prop === 'answers-for' && op === '=') {
|
|
852
932
|
if (value.startsWith('LL')) {
|
|
@@ -1033,6 +1113,14 @@ class LoincServices extends BaseCSServices {
|
|
|
1033
1113
|
else if (prop === 'concept' && ['is-a', 'descendent-of'].includes(op)) {
|
|
1034
1114
|
({ sql, lsql } = this.#hierarchyFilterSql(op, value));
|
|
1035
1115
|
}
|
|
1116
|
+
// ancestor = X is LOINC's name for descendent-of X: "Ancestor includes parents transitively",
|
|
1117
|
+
// and the part itself is not its own ancestor. in is the union over the parts
|
|
1118
|
+
else if (prop === 'ancestor' && op === '=') {
|
|
1119
|
+
({ sql, lsql } = this.#hierarchyFilterSql('descendent-of', value));
|
|
1120
|
+
}
|
|
1121
|
+
else if (prop === 'ancestor' && op === 'in') {
|
|
1122
|
+
({ sql, lsql } = this.#hierarchyFilterSql('descendent-of', null, this.#commaListOfCodes(value)));
|
|
1123
|
+
}
|
|
1036
1124
|
// Concept equal filter (workaround for VSAC misuse)
|
|
1037
1125
|
else if (prop === 'concept' && op === '=') {
|
|
1038
1126
|
sql = `SELECT CodeKey as Key FROM Codes
|
|
@@ -1109,14 +1197,15 @@ class LoincServices extends BaseCSServices {
|
|
|
1109
1197
|
* the file as a graph would, for example, put Glucose in Serum or Plasma under Urinalysis,
|
|
1110
1198
|
* because Protein appears under both.
|
|
1111
1199
|
*/
|
|
1112
|
-
#hierarchyFilterSql(op, value) {
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1200
|
+
#hierarchyFilterSql(op, value, codeList = null) {
|
|
1201
|
+
// one code, or (codeList) an already quoted, comma separated list of them
|
|
1202
|
+
const codes = codeList || `'${this.#sqlWrapString(value)}'`;
|
|
1203
|
+
const descendants = `SELECT DISTINCT DescendentKey as Key FROM Closure
|
|
1204
|
+
WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))`;
|
|
1116
1205
|
const set = op === 'is-a'
|
|
1117
1206
|
? `${descendants}
|
|
1118
1207
|
UNION
|
|
1119
|
-
SELECT CodeKey as Key FROM Codes WHERE Code
|
|
1208
|
+
SELECT CodeKey as Key FROM Codes WHERE Code IN (${codes})`
|
|
1120
1209
|
: descendants;
|
|
1121
1210
|
return {
|
|
1122
1211
|
sql: `${set}
|
|
@@ -1128,10 +1217,11 @@ class LoincServices extends BaseCSServices {
|
|
|
1128
1217
|
// Helper method for regex matching
|
|
1129
1218
|
async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') {
|
|
1130
1219
|
return new Promise((resolve, reject) => {
|
|
1131
|
-
|
|
1220
|
+
// the whole value, as for every regex filter: a|b is ^(?:a|b)$
|
|
1221
|
+
const regex = regexUtilities.compile('^(?:' + pattern + ')$');
|
|
1132
1222
|
const matchingKeys = [];
|
|
1133
1223
|
|
|
1134
|
-
this.db.all(sql, (err, rows) => {
|
|
1224
|
+
this.db.all(sql, guarded(reject, (err, rows) => {
|
|
1135
1225
|
if (err) {
|
|
1136
1226
|
reject(err);
|
|
1137
1227
|
} else {
|
|
@@ -1143,7 +1233,7 @@ class LoincServices extends BaseCSServices {
|
|
|
1143
1233
|
}
|
|
1144
1234
|
resolve(matchingKeys);
|
|
1145
1235
|
}
|
|
1146
|
-
});
|
|
1236
|
+
}));
|
|
1147
1237
|
});
|
|
1148
1238
|
}
|
|
1149
1239
|
|
|
@@ -1157,14 +1247,14 @@ class LoincServices extends BaseCSServices {
|
|
|
1157
1247
|
|
|
1158
1248
|
async #executeSQL(sql, filter) {
|
|
1159
1249
|
return new Promise((resolve, reject) => {
|
|
1160
|
-
this.db.all(sql, (err, rows) => {
|
|
1250
|
+
this.db.all(sql, guarded(reject, (err, rows) => {
|
|
1161
1251
|
if (err) {
|
|
1162
1252
|
reject(err);
|
|
1163
1253
|
} else {
|
|
1164
1254
|
filter.keys = rows.map(row => row.Key).filter(key => key !== 0);
|
|
1165
1255
|
resolve();
|
|
1166
1256
|
}
|
|
1167
|
-
});
|
|
1257
|
+
}));
|
|
1168
1258
|
});
|
|
1169
1259
|
}
|
|
1170
1260
|
|
|
@@ -1173,8 +1263,19 @@ class LoincServices extends BaseCSServices {
|
|
|
1173
1263
|
}
|
|
1174
1264
|
|
|
1175
1265
|
async executeFilters(filterContext) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1266
|
+
const filters = filterContext.filters;
|
|
1267
|
+
if (filters.length <= 1) {
|
|
1268
|
+
return filters;
|
|
1269
|
+
}
|
|
1270
|
+
// Every filter is already a list of keys in memory, so the include is their intersection,
|
|
1271
|
+
// as one set: its size is the include's size (count=0 needs no walk, and so no limit), and
|
|
1272
|
+
// the expander walks only the codes that are in it. In the first filter's order
|
|
1273
|
+
const others = filters.slice(1).map(f => new Set(f.keys));
|
|
1274
|
+
const set = new LoincFilterHolder();
|
|
1275
|
+
set.keys = filters[0].keys.filter(k => others.every(o => o.has(k)));
|
|
1276
|
+
set.keySet = new Set(set.keys);
|
|
1277
|
+
set.lsql = filters.map(f => f.lsql).find(l => l) || 'intersection';
|
|
1278
|
+
return [set];
|
|
1178
1279
|
}
|
|
1179
1280
|
|
|
1180
1281
|
async filterSize(filterContext, set) {
|
|
@@ -1285,9 +1386,14 @@ class LoincServices extends BaseCSServices {
|
|
|
1285
1386
|
}
|
|
1286
1387
|
|
|
1287
1388
|
class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
1288
|
-
|
|
1389
|
+
/**
|
|
1390
|
+
* @param {Object} [options] - {readOnly: true} in a tx-worker (cluster.md): the master
|
|
1391
|
+
* sets the journal mode and makes the indexes; this process opens the file read-only
|
|
1392
|
+
*/
|
|
1393
|
+
constructor(i18n, dbPath, options = {}) {
|
|
1289
1394
|
super(i18n);
|
|
1290
1395
|
this.dbPath = dbPath;
|
|
1396
|
+
this.readOnly = !!options.readOnly;
|
|
1291
1397
|
this.uses = 0;
|
|
1292
1398
|
this._loaded = false;
|
|
1293
1399
|
this._sharedData = null;
|
|
@@ -1312,7 +1418,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1312
1418
|
}
|
|
1313
1419
|
|
|
1314
1420
|
async load() {
|
|
1315
|
-
const db = new sqlite3.Database(this.dbPath);
|
|
1421
|
+
const db = this.readOnly ? new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY) : new sqlite3.Database(this.dbPath);
|
|
1316
1422
|
|
|
1317
1423
|
// Enable performance optimizations
|
|
1318
1424
|
await this.#optimizeDatabase(db);
|
|
@@ -1365,6 +1471,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1365
1471
|
async #optimizeDatabase(db) {
|
|
1366
1472
|
return new Promise((resolve, reject) => {
|
|
1367
1473
|
db.serialize(() => {
|
|
1474
|
+
if (this.readOnly) {
|
|
1475
|
+
// this connection's settings only; the file is the master's
|
|
1476
|
+
db.run('PRAGMA cache_size = 10000');
|
|
1477
|
+
db.run('PRAGMA temp_store = MEMORY');
|
|
1478
|
+
db.run('PRAGMA mmap_size = 268435456', (err) => err ? reject(err) : resolve());
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1368
1481
|
db.run('PRAGMA journal_mode = WAL');
|
|
1369
1482
|
db.run('PRAGMA synchronous = NORMAL');
|
|
1370
1483
|
db.run('PRAGMA cache_size = 10000');
|
|
@@ -1384,7 +1497,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1384
1497
|
|
|
1385
1498
|
async #loadLanguages(db) {
|
|
1386
1499
|
return new Promise((resolve, reject) => {
|
|
1387
|
-
db.all('SELECT LanguageKey, Code FROM Languages', (err, rows) => {
|
|
1500
|
+
db.all('SELECT LanguageKey, Code FROM Languages', guarded(reject, (err, rows) => {
|
|
1388
1501
|
if (err) {
|
|
1389
1502
|
reject(err);
|
|
1390
1503
|
} else {
|
|
@@ -1393,13 +1506,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1393
1506
|
}
|
|
1394
1507
|
resolve();
|
|
1395
1508
|
}
|
|
1396
|
-
});
|
|
1509
|
+
}));
|
|
1397
1510
|
});
|
|
1398
1511
|
}
|
|
1399
1512
|
|
|
1400
1513
|
async #loadStatusCodes(db) {
|
|
1401
1514
|
return new Promise((resolve, reject) => {
|
|
1402
|
-
db.all('SELECT StatusKey, Description FROM StatusCodes', (err, rows) => {
|
|
1515
|
+
db.all('SELECT StatusKey, Description FROM StatusCodes', guarded(reject, (err, rows) => {
|
|
1403
1516
|
if (err) {
|
|
1404
1517
|
reject(err);
|
|
1405
1518
|
} else {
|
|
@@ -1409,13 +1522,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1409
1522
|
}
|
|
1410
1523
|
resolve();
|
|
1411
1524
|
}
|
|
1412
|
-
});
|
|
1525
|
+
}));
|
|
1413
1526
|
});
|
|
1414
1527
|
}
|
|
1415
1528
|
|
|
1416
1529
|
async #loadRelationshipTypes(db) {
|
|
1417
1530
|
return new Promise((resolve, reject) => {
|
|
1418
|
-
db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', (err, rows) => {
|
|
1531
|
+
db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', guarded(reject, (err, rows) => {
|
|
1419
1532
|
if (err) {
|
|
1420
1533
|
reject(err);
|
|
1421
1534
|
} else {
|
|
@@ -1424,13 +1537,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1424
1537
|
}
|
|
1425
1538
|
resolve();
|
|
1426
1539
|
}
|
|
1427
|
-
});
|
|
1540
|
+
}));
|
|
1428
1541
|
});
|
|
1429
1542
|
}
|
|
1430
1543
|
|
|
1431
1544
|
async #loadPropertyTypes(db) {
|
|
1432
1545
|
return new Promise((resolve, reject) => {
|
|
1433
|
-
db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', (err, rows) => {
|
|
1546
|
+
db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', guarded(reject, (err, rows) => {
|
|
1434
1547
|
if (err) {
|
|
1435
1548
|
reject(err);
|
|
1436
1549
|
} else {
|
|
@@ -1439,14 +1552,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1439
1552
|
}
|
|
1440
1553
|
resolve();
|
|
1441
1554
|
}
|
|
1442
|
-
});
|
|
1555
|
+
}));
|
|
1443
1556
|
});
|
|
1444
1557
|
}
|
|
1445
1558
|
|
|
1446
1559
|
async #loadCodes(db) {
|
|
1447
1560
|
return new Promise((resolve, reject) => {
|
|
1448
1561
|
// First get the count to pre-allocate array
|
|
1449
|
-
db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', (err, row) => {
|
|
1562
|
+
db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', guarded(reject, (err, row) => {
|
|
1450
1563
|
if (err) return reject(err);
|
|
1451
1564
|
|
|
1452
1565
|
// Pre-allocate the array to avoid repeated resizing
|
|
@@ -1454,7 +1567,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1454
1567
|
this._sharedData.codeList = new Array(maxKey + 1).fill(null);
|
|
1455
1568
|
|
|
1456
1569
|
// Now load all codes
|
|
1457
|
-
db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by Type Asc, CodeKey Asc', (err, rows) => {
|
|
1570
|
+
db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by Type Asc, CodeKey Asc', guarded(reject, (err, rows) => {
|
|
1458
1571
|
if (err) return reject(err);
|
|
1459
1572
|
|
|
1460
1573
|
// Batch process rows
|
|
@@ -1476,8 +1589,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1476
1589
|
}
|
|
1477
1590
|
}
|
|
1478
1591
|
resolve();
|
|
1479
|
-
});
|
|
1480
|
-
});
|
|
1592
|
+
}));
|
|
1593
|
+
}));
|
|
1481
1594
|
});
|
|
1482
1595
|
}
|
|
1483
1596
|
|
|
@@ -1497,7 +1610,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1497
1610
|
ORDER BY d.CodeKey
|
|
1498
1611
|
`;
|
|
1499
1612
|
|
|
1500
|
-
db.all(sql, (err, rows) => {
|
|
1613
|
+
db.all(sql, guarded(reject, (err, rows) => {
|
|
1501
1614
|
if (err) return reject(err);
|
|
1502
1615
|
|
|
1503
1616
|
// Batch process by CodeKey to reduce lookups
|
|
@@ -1517,7 +1630,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1517
1630
|
}
|
|
1518
1631
|
}
|
|
1519
1632
|
resolve();
|
|
1520
|
-
});
|
|
1633
|
+
}));
|
|
1521
1634
|
});
|
|
1522
1635
|
}
|
|
1523
1636
|
|
|
@@ -1528,12 +1641,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1528
1641
|
}
|
|
1529
1642
|
|
|
1530
1643
|
return new Promise((resolve, reject) => {
|
|
1644
|
+
// DISTINCT, for databases imported with duplicate rows (see #addRelationshipProperties)
|
|
1531
1645
|
const sql = `
|
|
1532
|
-
SELECT SourceKey, TargetKey FROM Relationships
|
|
1646
|
+
SELECT DISTINCT SourceKey, TargetKey FROM Relationships
|
|
1533
1647
|
WHERE RelationshipTypeKey = ${childRelKey}
|
|
1534
1648
|
`;
|
|
1535
1649
|
|
|
1536
|
-
db.all(sql, (err, rows) => {
|
|
1650
|
+
db.all(sql, guarded(reject, (err, rows) => {
|
|
1537
1651
|
if (err) {
|
|
1538
1652
|
reject(err);
|
|
1539
1653
|
} else {
|
|
@@ -1547,13 +1661,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1547
1661
|
}
|
|
1548
1662
|
resolve();
|
|
1549
1663
|
}
|
|
1550
|
-
});
|
|
1664
|
+
}));
|
|
1551
1665
|
});
|
|
1552
1666
|
}
|
|
1553
1667
|
|
|
1554
1668
|
async #loadConfig(db) {
|
|
1555
1669
|
return new Promise((resolve, reject) => {
|
|
1556
|
-
db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', (err, rows) => {
|
|
1670
|
+
db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', guarded(reject, (err, rows) => {
|
|
1557
1671
|
if (err) {
|
|
1558
1672
|
reject(err);
|
|
1559
1673
|
} else {
|
|
@@ -1566,7 +1680,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1566
1680
|
}
|
|
1567
1681
|
resolve();
|
|
1568
1682
|
}
|
|
1569
|
-
});
|
|
1683
|
+
}));
|
|
1570
1684
|
});
|
|
1571
1685
|
}
|
|
1572
1686
|
|
|
@@ -1577,14 +1691,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1577
1691
|
// the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
|
|
1578
1692
|
async #loadHasMappings(db) {
|
|
1579
1693
|
return new Promise((resolve, reject) => {
|
|
1580
|
-
db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", (err, row) => {
|
|
1694
|
+
db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", guarded(reject, (err, row) => {
|
|
1581
1695
|
if (err) {
|
|
1582
1696
|
reject(err);
|
|
1583
1697
|
} else {
|
|
1584
1698
|
this._sharedData.hasMappings = !!row;
|
|
1585
1699
|
resolve();
|
|
1586
1700
|
}
|
|
1587
|
-
});
|
|
1701
|
+
}));
|
|
1588
1702
|
});
|
|
1589
1703
|
}
|
|
1590
1704
|
|
|
@@ -1597,7 +1711,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1597
1711
|
db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
|
|
1598
1712
|
FROM ExternalMappings, ExternalCodes, Codes
|
|
1599
1713
|
WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
1600
|
-
AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
|
|
1714
|
+
AND ExternalMappings.CodeKey = Codes.CodeKey`, guarded(reject, (err, rows) => {
|
|
1601
1715
|
if (err) {
|
|
1602
1716
|
reject(err);
|
|
1603
1717
|
} else {
|
|
@@ -1612,7 +1726,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1612
1726
|
}
|
|
1613
1727
|
resolve();
|
|
1614
1728
|
}
|
|
1615
|
-
});
|
|
1729
|
+
}));
|
|
1616
1730
|
});
|
|
1617
1731
|
}
|
|
1618
1732
|
|
|
@@ -1791,6 +1905,11 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1791
1905
|
id() {
|
|
1792
1906
|
return "loinc"+this.version();
|
|
1793
1907
|
}
|
|
1908
|
+
|
|
1909
|
+
/** Codes, parts, answers and groups all have a page at loinc.org (as LoincLinker in org.hl7.fhir.core) */
|
|
1910
|
+
codeLink(context) {
|
|
1911
|
+
return context && context.code ? `https://loinc.org/${encodeURIComponent(context.code)}/` : undefined;
|
|
1912
|
+
}
|
|
1794
1913
|
}
|
|
1795
1914
|
|
|
1796
1915
|
module.exports = {
|