fhirsmith 0.3.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/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { AbstractValueSetProvider } = require('./vs-api');
|
|
3
|
+
const { PackageContentLoader } = require('../../library/package-manager');
|
|
4
|
+
const { ValueSetDatabase } = require('./vs-database');
|
|
5
|
+
const { VersionUtilities } = require('../../library/version-utilities');
|
|
6
|
+
const {validateParameter} = require("../../library/utilities");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Package-based ValueSet provider using shared database layer
|
|
10
|
+
*/
|
|
11
|
+
class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
12
|
+
USE_DATABASE_SEARCH = true;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {PackageContentLoader} packageLoader - Path to the extracted package folder
|
|
16
|
+
*/
|
|
17
|
+
constructor(packageLoader) {
|
|
18
|
+
super();
|
|
19
|
+
validateParameter(packageLoader, "packageLoader", PackageContentLoader);
|
|
20
|
+
this.packageLoader = packageLoader;
|
|
21
|
+
this.dbPath = path.join(packageLoader.packageFolder, '.valuesets.db');
|
|
22
|
+
this.database = new ValueSetDatabase(this.dbPath);
|
|
23
|
+
this.valueSetMap = new Map();
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
this.count = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the provider - check/create database and load value sets into memory
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
async initialize() {
|
|
33
|
+
if (this.initialized) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await this.packageLoader.initialize();
|
|
37
|
+
|
|
38
|
+
const dbExists = await this.database.exists();
|
|
39
|
+
|
|
40
|
+
if (!dbExists) {
|
|
41
|
+
await this.database.create();
|
|
42
|
+
await this._populateDatabase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.valueSetMap = await this.database.loadAllValueSets(this.packageLoader.pid());
|
|
46
|
+
this.initialized = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async close() {
|
|
50
|
+
await this.database.close();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Populate the database with value sets from the package
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
async _populateDatabase() {
|
|
58
|
+
// Get all ValueSet resources
|
|
59
|
+
const valueSetEntries = await this.packageLoader.getResourcesByType('ValueSet');
|
|
60
|
+
|
|
61
|
+
if (valueSetEntries.length === 0) {
|
|
62
|
+
return; // No value sets in this package
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const valueSets = [];
|
|
66
|
+
for (const entry of valueSetEntries) {
|
|
67
|
+
const valueSet = await this.packageLoader.loadFile(entry);
|
|
68
|
+
if (valueSet.url) {
|
|
69
|
+
valueSets.push(valueSet);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (valueSets.length > 0) {
|
|
74
|
+
await this.database.batchUpsertValueSets(valueSets);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetches a value set by URL and version
|
|
80
|
+
* @param {string} url - The canonical URL of the value set
|
|
81
|
+
* @param {string} version - The version of the value set
|
|
82
|
+
* @returns {Promise<Object>} The requested value set
|
|
83
|
+
*/
|
|
84
|
+
async fetchValueSet(url, version) {
|
|
85
|
+
await this.initialize();
|
|
86
|
+
this._validateFetchParams(url, version);
|
|
87
|
+
|
|
88
|
+
// Try exact match first: url|version
|
|
89
|
+
let key = `${url}|${version}`;
|
|
90
|
+
if (this.valueSetMap.has(key)) {
|
|
91
|
+
return this.valueSetMap.get(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If version is semver, try url|major.minor
|
|
95
|
+
try {
|
|
96
|
+
if (VersionUtilities.isSemVer(version)) {
|
|
97
|
+
const majorMinor = VersionUtilities.getMajMin(version);
|
|
98
|
+
if (majorMinor) {
|
|
99
|
+
key = `${url}|${majorMinor}`;
|
|
100
|
+
if (this.valueSetMap.has(key)) {
|
|
101
|
+
return this.valueSetMap.get(key);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Ignore version parsing errors
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Finally try just the URL
|
|
110
|
+
if (this.valueSetMap.has(url)) {
|
|
111
|
+
return this.valueSetMap.get(url);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Searches for value sets based on criteria
|
|
119
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
|
120
|
+
* @returns {Promise<Array<Object>>} List of matching value sets
|
|
121
|
+
*/
|
|
122
|
+
async searchValueSets(searchParams, elements = null) {
|
|
123
|
+
await this.initialize();
|
|
124
|
+
this._validateSearchParams(searchParams);
|
|
125
|
+
|
|
126
|
+
if (this.USE_DATABASE_SEARCH) {
|
|
127
|
+
return await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
|
|
128
|
+
} else {
|
|
129
|
+
const matches = [];
|
|
130
|
+
const seen = new Set(); // Track by URL to avoid duplicates from versioned keys
|
|
131
|
+
|
|
132
|
+
// Convert array format to object for easier access
|
|
133
|
+
const params = {};
|
|
134
|
+
for (const {name, value} of searchParams) {
|
|
135
|
+
params[name] = value.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hasSearchParams = Object.keys(params).length > 0;
|
|
139
|
+
|
|
140
|
+
for (const vs of this.valueSetMap.values()) {
|
|
141
|
+
const json = vs.jsonObj || vs;
|
|
142
|
+
|
|
143
|
+
// Only process each ValueSet once (use URL to deduplicate)
|
|
144
|
+
const vsUrl = json.url;
|
|
145
|
+
if (seen.has(vsUrl)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!hasSearchParams) {
|
|
150
|
+
seen.add(vsUrl);
|
|
151
|
+
matches.push(vs);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check each search parameter
|
|
156
|
+
let isMatch = true;
|
|
157
|
+
for (const [param, searchValue] of Object.entries(params)) {
|
|
158
|
+
// Ignore content-mode and supplements for ValueSet search
|
|
159
|
+
if (param === 'content-mode' || param === 'supplements') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (param === 'system') {
|
|
164
|
+
// Special handling: match against compose.include[].system
|
|
165
|
+
if (!this._matchSystem(json, searchValue)) {
|
|
166
|
+
isMatch = false;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
} else if (param === 'jurisdiction') {
|
|
170
|
+
// Special handling for jurisdiction - array of CodeableConcept
|
|
171
|
+
if (!this._matchJurisdiction(json.jurisdiction, searchValue)) {
|
|
172
|
+
isMatch = false;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
} else if (param === 'identifier') {
|
|
176
|
+
// Special handling for identifier
|
|
177
|
+
if (!this._matchIdentifier(json.identifier, searchValue)) {
|
|
178
|
+
isMatch = false;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
} else if (param === 'url') {
|
|
182
|
+
const propValue = json[param];
|
|
183
|
+
if (!this._matchValueFull(propValue, searchValue)) {
|
|
184
|
+
isMatch = false;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Standard partial text match on property
|
|
189
|
+
const propValue = json[param];
|
|
190
|
+
if (!this._matchValue(propValue, searchValue)) {
|
|
191
|
+
isMatch = false;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isMatch) {
|
|
198
|
+
seen.add(vsUrl);
|
|
199
|
+
// Return with prefixed id
|
|
200
|
+
matches.push(json);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return matches;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if a value matches the search term (partial, case-insensitive)
|
|
210
|
+
*/
|
|
211
|
+
_matchValue(propValue, searchValue) {
|
|
212
|
+
if (propValue === undefined || propValue === null) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const strValue = String(propValue).toLowerCase();
|
|
216
|
+
return strValue.includes(searchValue);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a value matches the search term (partial, case-insensitive)
|
|
221
|
+
*/
|
|
222
|
+
_matchValueFull(propValue, searchValue) {
|
|
223
|
+
if (propValue === undefined || propValue === null) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const strValue = String(propValue).toLowerCase();
|
|
227
|
+
return strValue === searchValue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if system matches any compose.include[].system
|
|
232
|
+
*/
|
|
233
|
+
_matchSystem(json, searchValue) {
|
|
234
|
+
if (!json.compose?.include || !Array.isArray(json.compose.include)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
for (const include of json.compose.include) {
|
|
238
|
+
if (include.system && include.system.toLowerCase().includes(searchValue)) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if jurisdiction matches - jurisdiction is an array of CodeableConcept
|
|
247
|
+
*/
|
|
248
|
+
_matchJurisdiction(jurisdictions, searchValue) {
|
|
249
|
+
if (!jurisdictions || !Array.isArray(jurisdictions)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
for (const cc of jurisdictions) {
|
|
253
|
+
if (cc.coding && Array.isArray(cc.coding)) {
|
|
254
|
+
for (const coding of cc.coding) {
|
|
255
|
+
if (coding.code && coding.code.toLowerCase().includes(searchValue)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (coding.display && coding.display.toLowerCase().includes(searchValue)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (cc.text && cc.text.toLowerCase().includes(searchValue)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if identifier matches
|
|
272
|
+
*/
|
|
273
|
+
_matchIdentifier(identifiers, searchValue) {
|
|
274
|
+
if (!identifiers) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const idArray = Array.isArray(identifiers) ? identifiers : [identifiers];
|
|
278
|
+
for (const id of idArray) {
|
|
279
|
+
if (id.system && id.system.toLowerCase().includes(searchValue)) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (id.value && id.value.toLowerCase().includes(searchValue)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get statistics about the loaded value sets
|
|
291
|
+
* @returns {Promise<Object>} Statistics object
|
|
292
|
+
*/
|
|
293
|
+
async getStatistics() {
|
|
294
|
+
await this.initialize();
|
|
295
|
+
return await this.database.getStatistics();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the number of value sets loaded into memory
|
|
300
|
+
* @returns {number} Number of unique value sets in map
|
|
301
|
+
*/
|
|
302
|
+
getMapSize() {
|
|
303
|
+
const uniqueUrls = new Set();
|
|
304
|
+
for (const [key, valueSet] of this.valueSetMap.entries()) {
|
|
305
|
+
if (!key.includes('|')) { // Only count base URL keys
|
|
306
|
+
uniqueUrls.add(valueSet.url);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return uniqueUrls.size;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async fetchValueSetById(id) {
|
|
313
|
+
return this.valueSetMap.get(id);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// eslint-disable-next-line no-unused-vars
|
|
317
|
+
assignIds(ids) {
|
|
318
|
+
if (!this.spaceId) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const prefix = this.spaceId + '-';
|
|
323
|
+
const alreadyPrefixed = new Set();
|
|
324
|
+
|
|
325
|
+
// Get all current entries - we'll iterate and modify
|
|
326
|
+
const entries = Array.from(this.valueSetMap.entries());
|
|
327
|
+
|
|
328
|
+
for (const [key, vs] of entries) {
|
|
329
|
+
// Skip if we've already processed this ValueSet instance
|
|
330
|
+
if (alreadyPrefixed.has(vs)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Update the id on the ValueSet itself
|
|
335
|
+
if (vs.id && !vs.id.startsWith(prefix)) {
|
|
336
|
+
const oldId = vs.id;
|
|
337
|
+
vs.id = prefix + oldId;
|
|
338
|
+
|
|
339
|
+
// Add to map under the new id as well
|
|
340
|
+
this.valueSetMap.set(vs.id, vs);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
alreadyPrefixed.add(vs);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
vsCount() {
|
|
348
|
+
return this.database.vsCount;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async listAllValueSets() {
|
|
352
|
+
return await this.database.listAllValueSets();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
PackageValueSetProvider
|
|
358
|
+
};
|
package/tx/vs/vs-vsac.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const { AbstractValueSetProvider } = require('./vs-api');
|
|
4
|
+
const { ValueSetDatabase } = require('./vs-database');
|
|
5
|
+
const { VersionUtilities } = require('../../library/version-utilities');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* VSAC (Value Set Authority Center) ValueSet provider
|
|
9
|
+
* Fetches and caches ValueSets from the NLM VSAC FHIR server
|
|
10
|
+
*/
|
|
11
|
+
class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} config - Configuration object
|
|
14
|
+
* @param {string} config.apiKey - API key for VSAC authentication
|
|
15
|
+
* @param {string} config.cacheFolder - Local folder for cached database
|
|
16
|
+
* @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans
|
|
17
|
+
* @param {string} [config.baseUrl='http://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
if (!config.apiKey) {
|
|
23
|
+
throw new Error('API key is required');
|
|
24
|
+
}
|
|
25
|
+
if (!config.cacheFolder) {
|
|
26
|
+
throw new Error('Cache folder is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.apiKey = config.apiKey;
|
|
30
|
+
this.cacheFolder = config.cacheFolder;
|
|
31
|
+
this.baseUrl = config.baseUrl || 'http://cts.nlm.nih.gov/fhir';
|
|
32
|
+
this.refreshIntervalHours = config.refreshIntervalHours || 24;
|
|
33
|
+
|
|
34
|
+
this.dbPath = path.join(config.cacheFolder, 'vsac-valuesets.db');
|
|
35
|
+
this.database = new ValueSetDatabase(this.dbPath);
|
|
36
|
+
this.valueSetMap = new Map();
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
this.refreshTimer = null;
|
|
39
|
+
this.isRefreshing = false;
|
|
40
|
+
this.lastRefresh = null;
|
|
41
|
+
|
|
42
|
+
// HTTP client with authentication - manually create Basic auth header
|
|
43
|
+
const authString = Buffer.from(`apikey:${this.apiKey}`).toString('base64');
|
|
44
|
+
this.httpClient = axios.create({
|
|
45
|
+
baseURL: this.baseUrl,
|
|
46
|
+
timeout: 30000,
|
|
47
|
+
headers: {
|
|
48
|
+
'Accept': 'application/fhir+json',
|
|
49
|
+
'User-Agent': 'FHIR-ValueSet-Provider/1.0',
|
|
50
|
+
'Authorization': `Basic ${authString}`
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize the provider - setup database and start refresh cycle
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async initialize() {
|
|
60
|
+
if (this.initialized) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create database if it doesn't exist
|
|
65
|
+
if (!(await this.database.exists())) {
|
|
66
|
+
await this.database.create();
|
|
67
|
+
// Force initial refresh for new database
|
|
68
|
+
await this.refreshValueSets();
|
|
69
|
+
} else {
|
|
70
|
+
// Load existing data
|
|
71
|
+
await this._reloadMap();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Start periodic refresh
|
|
75
|
+
this._startRefreshTimer();
|
|
76
|
+
this.initialized = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start the periodic refresh timer
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_startRefreshTimer() {
|
|
84
|
+
if (this.refreshTimer) {
|
|
85
|
+
clearInterval(this.refreshTimer);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const intervalMs = this.refreshIntervalHours * 60 * 60 * 1000;
|
|
89
|
+
this.refreshTimer = setInterval(async () => {
|
|
90
|
+
try {
|
|
91
|
+
await this.refreshValueSets();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.log.error(error, 'Error during scheduled refresh:');
|
|
94
|
+
}
|
|
95
|
+
}, intervalMs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stop the refresh timer (for cleanup)
|
|
100
|
+
*/
|
|
101
|
+
stopRefreshTimer() {
|
|
102
|
+
if (this.refreshTimer) {
|
|
103
|
+
clearInterval(this.refreshTimer);
|
|
104
|
+
this.refreshTimer = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Perform a full refresh of ValueSets from the server
|
|
110
|
+
* @returns {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
async refreshValueSets() {
|
|
113
|
+
if (this.isRefreshing) {
|
|
114
|
+
console.log('Refresh already in progress, skipping');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.isRefreshing = true;
|
|
119
|
+
const refreshStartTime = Math.floor(Date.now() / 1000);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
console.log('Starting VSAC ValueSet refresh...');
|
|
123
|
+
|
|
124
|
+
let totalFetched = 0;
|
|
125
|
+
let url = '/ValueSet?_offset=0&_count=100';
|
|
126
|
+
|
|
127
|
+
while (url) {
|
|
128
|
+
console.log(`Fetching page: ${url}`);
|
|
129
|
+
const bundle = await this._fetchBundle(url);
|
|
130
|
+
|
|
131
|
+
if (bundle.entry && bundle.entry.length > 0) {
|
|
132
|
+
// Extract ValueSets from bundle entries
|
|
133
|
+
const valueSets = bundle.entry
|
|
134
|
+
.filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
|
|
135
|
+
.map(entry => entry.resource);
|
|
136
|
+
|
|
137
|
+
if (valueSets.length > 0) {
|
|
138
|
+
await this.database.batchUpsertValueSets(valueSets);
|
|
139
|
+
totalFetched += valueSets.length;
|
|
140
|
+
console.log(`Processed ${valueSets.length} ValueSets (total: ${totalFetched})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Find next link
|
|
145
|
+
url = this._getNextUrl(bundle);
|
|
146
|
+
|
|
147
|
+
// Safety check against infinite loops
|
|
148
|
+
if (bundle.total && totalFetched >= bundle.total) {
|
|
149
|
+
console.log(`Reached total count (${bundle.total}), stopping`);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Clean up old records
|
|
155
|
+
const deletedCount = await this.database.deleteOldValueSets(refreshStartTime);
|
|
156
|
+
if (deletedCount > 0) {
|
|
157
|
+
console.log(`Deleted ${deletedCount} old ValueSets`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reload map with fresh data
|
|
161
|
+
await this._reloadMap();
|
|
162
|
+
|
|
163
|
+
this.lastRefresh = new Date();
|
|
164
|
+
console.log(`VSAC refresh completed. Total: ${totalFetched} ValueSets, Deleted: ${deletedCount}`);
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.log(error, 'Error during VSAC refresh:');
|
|
168
|
+
throw error;
|
|
169
|
+
} finally {
|
|
170
|
+
this.isRefreshing = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fetch a FHIR Bundle from the server
|
|
176
|
+
* @param {string} url - Relative URL to fetch
|
|
177
|
+
* @returns {Promise<Object>} FHIR Bundle
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
async _fetchBundle(url) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await this.httpClient.get(url);
|
|
183
|
+
|
|
184
|
+
if (response.data && response.data.resourceType === 'Bundle') {
|
|
185
|
+
return response.data;
|
|
186
|
+
} else {
|
|
187
|
+
throw new Error('Response is not a FHIR Bundle');
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.response) {
|
|
191
|
+
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
192
|
+
} else if (error.request) {
|
|
193
|
+
throw new Error('Network error: No response received');
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error(`Request error: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extract the next URL from a FHIR Bundle's link array
|
|
202
|
+
* @param {Object} bundle - FHIR Bundle
|
|
203
|
+
* @returns {string|null} Next URL or null if no more pages
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_getNextUrl(bundle) {
|
|
207
|
+
if (!bundle.link || !Array.isArray(bundle.link)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const nextLink = bundle.link.find(link => link.relation === 'next');
|
|
212
|
+
if (!nextLink || !nextLink.url) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Extract relative path from full URL
|
|
217
|
+
let s = nextLink.url;
|
|
218
|
+
s = s.replace(this.baseUrl, '');
|
|
219
|
+
return s;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reload the in-memory map from database (thread-safe)
|
|
224
|
+
* @returns {Promise<void>}
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
async _reloadMap() {
|
|
228
|
+
const newMap = await this.database.loadAllValueSets("vsac");
|
|
229
|
+
|
|
230
|
+
// Atomic replacement of the map
|
|
231
|
+
this.valueSetMap = newMap;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Fetches a value set by URL and version
|
|
236
|
+
* @param {string} url - The canonical URL of the value set
|
|
237
|
+
* @param {string} version - The version of the value set
|
|
238
|
+
* @returns {Promise<Object>} The requested value set
|
|
239
|
+
*/
|
|
240
|
+
async fetchValueSet(url, version) {
|
|
241
|
+
await this.initialize();
|
|
242
|
+
this._validateFetchParams(url, version);
|
|
243
|
+
|
|
244
|
+
// Try exact match first: url|version
|
|
245
|
+
let key = `${url}|${version}`;
|
|
246
|
+
if (this.valueSetMap.has(key)) {
|
|
247
|
+
return this.valueSetMap.get(key);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If version is semver, try url|major.minor
|
|
251
|
+
try {
|
|
252
|
+
if (VersionUtilities.isSemVer(version)) {
|
|
253
|
+
const majorMinor = VersionUtilities.getMajMin(version);
|
|
254
|
+
if (majorMinor) {
|
|
255
|
+
key = `${url}|${majorMinor}`;
|
|
256
|
+
if (this.valueSetMap.has(key)) {
|
|
257
|
+
return this.valueSetMap.get(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Ignore version parsing errors
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Finally try just the URL
|
|
266
|
+
if (this.valueSetMap.has(url)) {
|
|
267
|
+
return this.valueSetMap.get(url);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw new Error(`Value set not found: ${url} version ${version}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Searches for value sets based on criteria
|
|
275
|
+
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
|
276
|
+
* @returns {Promise<Array<Object>>} List of matching value sets
|
|
277
|
+
*/
|
|
278
|
+
async searchValueSets(searchParams, elements) {
|
|
279
|
+
await this.initialize();
|
|
280
|
+
this._validateSearchParams(searchParams);
|
|
281
|
+
|
|
282
|
+
if (searchParams.length === 0) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return await this.database.search(this.spaceId, this.valueSetMap, searchParams, elements);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get statistics about the cached ValueSets
|
|
291
|
+
* @returns {Promise<Object>} Statistics object including refresh info
|
|
292
|
+
*/
|
|
293
|
+
async getStatistics() {
|
|
294
|
+
await this.initialize();
|
|
295
|
+
|
|
296
|
+
const dbStats = await this.database.getStatistics();
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
...dbStats,
|
|
300
|
+
refreshInfo: {
|
|
301
|
+
lastRefresh: this.lastRefresh,
|
|
302
|
+
isRefreshing: this.isRefreshing,
|
|
303
|
+
refreshIntervalHours: this.refreshIntervalHours,
|
|
304
|
+
nextRefresh: this.refreshTimer && this.lastRefresh
|
|
305
|
+
? new Date(this.lastRefresh.getTime() + (this.refreshIntervalHours * 60 * 60 * 1000))
|
|
306
|
+
: null
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get the number of value sets loaded into memory
|
|
313
|
+
* @returns {number} Number of unique value sets in map
|
|
314
|
+
*/
|
|
315
|
+
getMapSize() {
|
|
316
|
+
const uniqueUrls = new Set();
|
|
317
|
+
for (const [key, valueSet] of this.valueSetMap.entries()) {
|
|
318
|
+
if (!key.includes('|')) { // Only count base URL keys
|
|
319
|
+
uniqueUrls.add(valueSet.url);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return uniqueUrls.size;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Force a refresh (useful for testing or manual updates)
|
|
327
|
+
* @returns {Promise<void>}
|
|
328
|
+
*/
|
|
329
|
+
async forceRefresh() {
|
|
330
|
+
await this.refreshValueSets();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Check if the provider is currently refreshing
|
|
335
|
+
* @returns {boolean} True if refresh is in progress
|
|
336
|
+
*/
|
|
337
|
+
isCurrentlyRefreshing() {
|
|
338
|
+
return this.isRefreshing;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get the last refresh timestamp
|
|
343
|
+
* @returns {Date|null} Last refresh date or null if never refreshed
|
|
344
|
+
*/
|
|
345
|
+
getLastRefreshTime() {
|
|
346
|
+
return this.lastRefresh;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
count() {
|
|
350
|
+
return this.database.vsCount;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async listAllValueSets() {
|
|
354
|
+
return await this.database.listAllValueSets();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async close() {
|
|
358
|
+
await this.database.close();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Usage examples:
|
|
364
|
+
module.exports = {
|
|
365
|
+
VSACValueSetProvider
|
|
366
|
+
};
|