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,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackageManager - FHIR Package management with caching
|
|
3
|
+
* Fetches and caches FHIR packages from package servers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const { URL } = require('url');
|
|
11
|
+
const zlib = require('zlib');
|
|
12
|
+
const tar = require('tar');
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
const { VersionUtilities } = require('../library/version-utilities');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ROOT_URL = 'https://build.fhir.org';
|
|
17
|
+
const DEFAULT_CI_QUERY_INTERVAL = 1000 * 60 * 60; // 1 hour
|
|
18
|
+
|
|
19
|
+
class CIBuildClient {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} rootUrl - Base URL for CI build server
|
|
22
|
+
* @param {number} ciQueryInterval - Interval between server queries in ms
|
|
23
|
+
*/
|
|
24
|
+
constructor(rootUrl = DEFAULT_ROOT_URL, ciQueryInterval = DEFAULT_CI_QUERY_INTERVAL) {
|
|
25
|
+
this.rootUrl = rootUrl;
|
|
26
|
+
this.ciQueryInterval = ciQueryInterval;
|
|
27
|
+
this.ciLastQueriedTimeStamp = 0;
|
|
28
|
+
this.ciBuildInfo = null;
|
|
29
|
+
|
|
30
|
+
// key = packageId, value = url of built package on build.fhir.org/ig/
|
|
31
|
+
this.ciPackageUrls = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get package ID from canonical URL
|
|
36
|
+
* @param {string} canonical - Canonical URL
|
|
37
|
+
* @returns {Promise<string|null>} Package ID or null
|
|
38
|
+
*/
|
|
39
|
+
async getPackageId(canonical) {
|
|
40
|
+
if (!canonical) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await this.checkCIServerQueried();
|
|
45
|
+
|
|
46
|
+
if (this.ciBuildInfo) {
|
|
47
|
+
// First pass: exact match
|
|
48
|
+
for (const o of this.ciBuildInfo) {
|
|
49
|
+
if (canonical === o.url) {
|
|
50
|
+
return o['package-id'];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Second pass: starts with canonical + /ImplementationGuide/
|
|
55
|
+
for (const o of this.ciBuildInfo) {
|
|
56
|
+
if (o.url && o.url.startsWith(canonical + '/ImplementationGuide/')) {
|
|
57
|
+
return o['package-id'];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get package URL from package ID
|
|
67
|
+
* @param {string} packageId - Package ID
|
|
68
|
+
* @returns {Promise<string|null>} Package URL or null
|
|
69
|
+
*/
|
|
70
|
+
async getPackageUrl(packageId) {
|
|
71
|
+
await this.checkCIServerQueried();
|
|
72
|
+
|
|
73
|
+
for (const o of this.ciBuildInfo || []) {
|
|
74
|
+
if (packageId === o['package-id']) {
|
|
75
|
+
return o.url;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if local package is current with CI build
|
|
84
|
+
* @param {string} id - Package ID
|
|
85
|
+
* @param {Object} npmPackage - Local npm package with date() method
|
|
86
|
+
* @returns {Promise<boolean>} True if current
|
|
87
|
+
*/
|
|
88
|
+
async isCurrent(id, npmPackage) {
|
|
89
|
+
await this.checkCIServerQueried();
|
|
90
|
+
|
|
91
|
+
const packageManifestUrl = this.ciPackageUrls.get(id);
|
|
92
|
+
if (!packageManifestUrl) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const manifestUrl = this.pathURL(packageManifestUrl, 'package.manifest.json');
|
|
97
|
+
const packageManifestJson = await this.fetchJson(manifestUrl);
|
|
98
|
+
const currentDate = packageManifestJson.date;
|
|
99
|
+
const packageDate = typeof npmPackage.date === 'function' ? npmPackage.date() : npmPackage.date;
|
|
100
|
+
|
|
101
|
+
return currentDate === packageDate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load package from CI build
|
|
106
|
+
* @param {string} id - Package ID
|
|
107
|
+
* @param {string} branch - Branch name (optional)
|
|
108
|
+
* @returns {Promise<{stream: Buffer, url: string, version: string}>}
|
|
109
|
+
*/
|
|
110
|
+
async loadFromCIBuild(id, branch = null) {
|
|
111
|
+
await this.checkCIServerQueried();
|
|
112
|
+
|
|
113
|
+
if (this.ciPackageUrls.has(id)) {
|
|
114
|
+
const packageBaseUrl = this.ciPackageUrls.get(id);
|
|
115
|
+
|
|
116
|
+
if (!branch) {
|
|
117
|
+
let stream;
|
|
118
|
+
let url = this.pathURL(packageBaseUrl, 'package.tgz');
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
stream = await this.fetchFromUrlSpecific(url);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
url = this.pathURL(packageBaseUrl, 'branches', 'main', 'package.tgz');
|
|
124
|
+
stream = await this.fetchFromUrlSpecific(url);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
stream,
|
|
129
|
+
url: this.pathURL(packageBaseUrl, 'package.tgz'),
|
|
130
|
+
version: 'current'
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
const url = this.pathURL(packageBaseUrl, 'branches', branch, 'package.tgz');
|
|
134
|
+
const stream = await this.fetchFromUrlSpecific(url);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
stream,
|
|
138
|
+
url,
|
|
139
|
+
version: 'current$' + branch
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} else if (id.startsWith('hl7.fhir.r6')) {
|
|
143
|
+
const url = this.pathURL(this.rootUrl, id + '.tgz');
|
|
144
|
+
const stream = await this.fetchFromUrlSpecific(url);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
stream,
|
|
148
|
+
url,
|
|
149
|
+
version: 'current'
|
|
150
|
+
};
|
|
151
|
+
} else if (this.endsWithInList(id, '.r3', '.r4', '.r4b', '.r5', '.r6')) {
|
|
152
|
+
const npid = id.substring(0, id.lastIndexOf('.'));
|
|
153
|
+
const baseUrl = this.ciPackageUrls.get(npid);
|
|
154
|
+
|
|
155
|
+
if (!baseUrl) {
|
|
156
|
+
throw new Error(`The package '${id}' has no entry on the current build server`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const url = this.pathURL(baseUrl, id + '.tgz');
|
|
160
|
+
const stream = await this.fetchFromUrlSpecific(url);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
stream,
|
|
164
|
+
url,
|
|
165
|
+
version: 'current'
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(`The package '${id}' has no entry on the current build server`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Fetch content from URL
|
|
174
|
+
* @param {string} source - URL to fetch
|
|
175
|
+
* @returns {Promise<Buffer>}
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
async fetchFromUrlSpecific(source) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const protocol = source.startsWith('https') ? https : http;
|
|
181
|
+
|
|
182
|
+
const request = protocol.get(source, (response) => {
|
|
183
|
+
// Handle redirects
|
|
184
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
185
|
+
this.fetchFromUrlSpecific(response.headers.location)
|
|
186
|
+
.then(resolve)
|
|
187
|
+
.catch(reject);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (response.statusCode !== 200) {
|
|
192
|
+
reject(new Error(`Unable to fetch ${source}: HTTP ${response.statusCode}`));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const chunks = [];
|
|
197
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
198
|
+
response.on('end', () => resolve(Buffer.concat(chunks)));
|
|
199
|
+
response.on('error', reject);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
request.on('error', (e) => reject(new Error(`Unable to fetch ${source}: ${e.message}`)));
|
|
203
|
+
request.setTimeout(30000, () => {
|
|
204
|
+
request.destroy();
|
|
205
|
+
reject(new Error(`Timeout fetching ${source}`));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetch JSON from URL
|
|
212
|
+
* @param {string} url - URL to fetch
|
|
213
|
+
* @returns {Promise<Object>}
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
async fetchJson(url) {
|
|
217
|
+
const buffer = await this.fetchFromUrlSpecific(url);
|
|
218
|
+
return JSON.parse(buffer.toString('utf8'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if CI server needs to be queried and update if needed
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
async checkCIServerQueried() {
|
|
226
|
+
if (Date.now() - this.ciLastQueriedTimeStamp > this.ciQueryInterval) {
|
|
227
|
+
try {
|
|
228
|
+
await this.updateFromCIServer();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Pause and retry once - most common reason is file being changed on server
|
|
231
|
+
await this.sleep(1000);
|
|
232
|
+
try {
|
|
233
|
+
await this.updateFromCIServer();
|
|
234
|
+
} catch (e2) {
|
|
235
|
+
console.debug(`Error connecting to build server - running without build (${e2.message})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Update package information from CI server
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
async updateFromCIServer() {
|
|
246
|
+
try {
|
|
247
|
+
const url = `${this.rootUrl}/ig/qas.json?nocache=${Date.now()}`;
|
|
248
|
+
const buffer = await this.fetchFromUrlSpecific(url);
|
|
249
|
+
this.ciBuildInfo = JSON.parse(buffer.toString('utf8'));
|
|
250
|
+
|
|
251
|
+
const builds = [];
|
|
252
|
+
|
|
253
|
+
for (const j of this.ciBuildInfo) {
|
|
254
|
+
if (j.url && j['package-id'] && j['package-id'].includes('.')) {
|
|
255
|
+
let packageUrl = j.url;
|
|
256
|
+
if (packageUrl.includes('/ImplementationGuide/')) {
|
|
257
|
+
packageUrl = packageUrl.substring(0, packageUrl.indexOf('/ImplementationGuide/'));
|
|
258
|
+
}
|
|
259
|
+
builds.push({
|
|
260
|
+
url: packageUrl,
|
|
261
|
+
packageId: j['package-id'],
|
|
262
|
+
repo: this.getRepo(j.repo),
|
|
263
|
+
date: this.readDate(j.date)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Sort by date descending (newest first)
|
|
269
|
+
builds.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
270
|
+
|
|
271
|
+
for (const build of builds) {
|
|
272
|
+
if (!this.ciPackageUrls.has(build.packageId)) {
|
|
273
|
+
this.ciPackageUrls.set(build.packageId, `${this.rootUrl}/ig/${build.repo}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
this.ciLastQueriedTimeStamp = Date.now();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract repo path from full path
|
|
283
|
+
* @param {string} path - Full path
|
|
284
|
+
* @returns {string} Repo path (org/repo)
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
getRepo(path) {
|
|
288
|
+
if (!path) return '';
|
|
289
|
+
const p = path.split('/');
|
|
290
|
+
return p[0] + '/' + p[1];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Parse date string from CI server
|
|
295
|
+
* @param {string} s - Date string in format "EEE, dd MMM, yyyy HH:mm:ss Z"
|
|
296
|
+
* @returns {Date}
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
readDate(s) {
|
|
300
|
+
if (!s) return new Date();
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Parse format like "Mon, 15 Jan, 2024 10:30:00 +0000"
|
|
304
|
+
return new Date(s);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.error('Error parsing date:', e);
|
|
307
|
+
return new Date();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Join URL path segments
|
|
313
|
+
* @param {...string} parts - Path parts
|
|
314
|
+
* @returns {string}
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
pathURL(...parts) {
|
|
318
|
+
return parts
|
|
319
|
+
.map((part, index) => {
|
|
320
|
+
if (index === 0) {
|
|
321
|
+
return part.replace(/\/+$/, '');
|
|
322
|
+
}
|
|
323
|
+
return part.replace(/^\/+|\/+$/g, '');
|
|
324
|
+
})
|
|
325
|
+
.filter(part => part.length > 0)
|
|
326
|
+
.join('/');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check if string ends with any of the given suffixes
|
|
331
|
+
* @param {string} str - String to check
|
|
332
|
+
* @param {...string} suffixes - Suffixes to check
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
* @private
|
|
335
|
+
*/
|
|
336
|
+
endsWithInList(str, ...suffixes) {
|
|
337
|
+
return suffixes.some(suffix => str.endsWith(suffix));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sleep for specified milliseconds
|
|
342
|
+
* @param {number} ms - Milliseconds to sleep
|
|
343
|
+
* @returns {Promise<void>}
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
sleep(ms) {
|
|
347
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class PackageManager {
|
|
353
|
+
totalDownloaded = 0;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {string[]} packageServers - Ordered list of package server URLs
|
|
357
|
+
* @param {string} cacheFolder - Local folder for cached content
|
|
358
|
+
*/
|
|
359
|
+
constructor(packageServers, cacheFolder) {
|
|
360
|
+
if (!packageServers || packageServers.length === 0) {
|
|
361
|
+
throw new Error('At least one package server must be provided');
|
|
362
|
+
}
|
|
363
|
+
this.packageServers = packageServers;
|
|
364
|
+
this.cacheFolder = cacheFolder;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Fetch a package, either from cache or from servers
|
|
369
|
+
* @param {string} packageId - Package identifier (e.g., 'hl7.fhir.us.core')
|
|
370
|
+
* @param {string} version - Version string (may contain wildcards)
|
|
371
|
+
* @returns {Promise<string>} Path to extracted package folder
|
|
372
|
+
*/
|
|
373
|
+
async fetch(packageId, version) {
|
|
374
|
+
// First, resolve the version if it contains wildcards
|
|
375
|
+
const resolvedVersion = await this.resolveVersion(packageId, version);
|
|
376
|
+
|
|
377
|
+
// Check cache first
|
|
378
|
+
const cachedPath = await this.checkCache(packageId, resolvedVersion);
|
|
379
|
+
if (cachedPath) {
|
|
380
|
+
return cachedPath;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log("Fetch Package "+packageId+"#"+version);
|
|
384
|
+
// Not in cache, fetch from servers
|
|
385
|
+
const packageData = await this.fetchFromServers(packageId, resolvedVersion);
|
|
386
|
+
|
|
387
|
+
this.totalDownloaded = this.totalDownloaded + packageData.length;
|
|
388
|
+
// Extract to cache
|
|
389
|
+
const extractedPath = await this.extractToCache(packageId, resolvedVersion, packageData);
|
|
390
|
+
|
|
391
|
+
return extractedPath;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolve version with wildcards to a specific version
|
|
396
|
+
* @param {string} packageId - Package identifier
|
|
397
|
+
* @param {string} version - Version string (may contain wildcards)
|
|
398
|
+
* @returns {Promise<string>} Resolved specific version
|
|
399
|
+
*/
|
|
400
|
+
async resolveVersion(packageId, version) {
|
|
401
|
+
// If no wildcards, return as-is
|
|
402
|
+
if (!VersionUtilities.versionHasWildcards(version) && version != null) {
|
|
403
|
+
return version;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Need to get version list and find best match
|
|
407
|
+
for (const server of this.packageServers) {
|
|
408
|
+
try {
|
|
409
|
+
const versions = await this.getPackageVersions(server, packageId);
|
|
410
|
+
const resolvedVersion = this.selectBestVersion(versions, version);
|
|
411
|
+
if (resolvedVersion) {
|
|
412
|
+
return resolvedVersion;
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
// Try next server
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
throw new Error(`Could not resolve version ${version} for package ${packageId}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get list of available versions for a package from a server
|
|
425
|
+
* @param {string} server - Server URL
|
|
426
|
+
* @param {string} packageId - Package identifier
|
|
427
|
+
* @returns {Promise<string[]>} Array of version strings
|
|
428
|
+
*/
|
|
429
|
+
async getPackageVersions(server, packageId) {
|
|
430
|
+
const url = `${server}/${packageId}`;
|
|
431
|
+
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const parsedUrl = new URL(url);
|
|
434
|
+
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
435
|
+
|
|
436
|
+
const req = client.get(url, {
|
|
437
|
+
headers: {
|
|
438
|
+
'Accept': 'application/json'
|
|
439
|
+
}
|
|
440
|
+
}, (res) => {
|
|
441
|
+
if (res.statusCode === 404) {
|
|
442
|
+
reject(new Error(`Package ${packageId} not found on ${server}`));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (res.statusCode !== 200) {
|
|
447
|
+
reject(new Error(`HTTP ${res.statusCode} from ${server}`));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let data = '';
|
|
452
|
+
res.on('data', chunk => data += chunk);
|
|
453
|
+
res.on('end', () => {
|
|
454
|
+
try {
|
|
455
|
+
const json = JSON.parse(data);
|
|
456
|
+
const versions = Object.keys(json.versions || {});
|
|
457
|
+
resolve(versions);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
reject(new Error(`Invalid JSON from ${server}: ${error.message}`));
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
req.on('error', reject);
|
|
465
|
+
req.end();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Select the best matching version from available versions
|
|
471
|
+
* @param {string[]} availableVersions - List of available versions
|
|
472
|
+
* @param {string} criteria - Version criteria (may contain wildcards)
|
|
473
|
+
* @returns {string|null} Best matching version or null if none match
|
|
474
|
+
*/
|
|
475
|
+
selectBestVersion(availableVersions, criteria) {
|
|
476
|
+
const sortedVersions = [...availableVersions].sort((a, b) => {
|
|
477
|
+
try {
|
|
478
|
+
return -VersionUtilities.compareVersions(a, b);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (criteria == null) {
|
|
485
|
+
return sortedVersions.length == 0 ? null : sortedVersions[0];
|
|
486
|
+
}
|
|
487
|
+
// Filter versions that match the criteria
|
|
488
|
+
const matchingVersions = sortedVersions.filter(v => {
|
|
489
|
+
try {
|
|
490
|
+
return VersionUtilities.versionMatches(criteria, v);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (matchingVersions.length === 0) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Sort by version (newest first) using compareVersions
|
|
501
|
+
matchingVersions.sort((a, b) => {
|
|
502
|
+
try {
|
|
503
|
+
return -VersionUtilities.compareVersions(a, b);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return matchingVersions[0];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Check if package exists in cache
|
|
514
|
+
* @param {string} packageId - Package identifier
|
|
515
|
+
* @param {string} version - Specific version
|
|
516
|
+
* @returns {Promise<string|null>} Path to cached package or null if not found
|
|
517
|
+
*/
|
|
518
|
+
async checkCache(packageId, version) {
|
|
519
|
+
const packageName = `${packageId}#${version}`;
|
|
520
|
+
const packagePath = path.join(this.cacheFolder, packageName);
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const stats = await fs.stat(packagePath);
|
|
524
|
+
if (stats.isDirectory()) {
|
|
525
|
+
return packageName;
|
|
526
|
+
}
|
|
527
|
+
} catch (error) {
|
|
528
|
+
// Not found or not accessible
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Fetch package data from servers
|
|
536
|
+
* @param {string} packageId - Package identifier
|
|
537
|
+
* @param {string} version - Specific version
|
|
538
|
+
* @returns {Promise<Buffer>} Package tar.gz data
|
|
539
|
+
*/
|
|
540
|
+
async fetchFromServers(packageId, version) {
|
|
541
|
+
let lastError = null;
|
|
542
|
+
|
|
543
|
+
if (version == "current") {
|
|
544
|
+
const result = await new CIBuildClient().loadFromCIBuild(packageId);
|
|
545
|
+
return result.stream;
|
|
546
|
+
}
|
|
547
|
+
for (const server of this.packageServers) {
|
|
548
|
+
try {
|
|
549
|
+
const packageData = await this.fetchFromServer(server, packageId, version);
|
|
550
|
+
return packageData;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
lastError = error;
|
|
553
|
+
// Try next server
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
throw new Error(`Failed to fetch ${packageId}#${version} from any server. Last error: ${lastError?.message}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Fetch package data from a specific server
|
|
563
|
+
* @param {string} server - Server URL
|
|
564
|
+
* @param {string} packageId - Package identifier
|
|
565
|
+
* @param {string} version - Specific version
|
|
566
|
+
* @returns {Promise<Buffer>} Package tar.gz data
|
|
567
|
+
*/
|
|
568
|
+
async fetchFromServer(server, packageId, version) {
|
|
569
|
+
const url = `${server}/${packageId}/${version}`;
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const response = await axios.get(url, {
|
|
573
|
+
headers: {
|
|
574
|
+
'Accept': 'application/tar+gzip'
|
|
575
|
+
},
|
|
576
|
+
responseType: 'arraybuffer',
|
|
577
|
+
maxRedirects: 5
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return Buffer.from(response.data);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error.response?.status === 404) {
|
|
583
|
+
throw new Error(`Package ${packageId}#${version} not found on ${server}`);
|
|
584
|
+
}
|
|
585
|
+
throw new Error(`HTTP ${error.response?.status || 'error'} from ${server}: ${error.message}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Extract package to cache folder
|
|
591
|
+
* @param {string} packageId - Package identifier
|
|
592
|
+
* @param {string} version - Specific version
|
|
593
|
+
* @param {Buffer} packageData - Package tar.gz data
|
|
594
|
+
* @returns {Promise<string>} Path to extracted package
|
|
595
|
+
*/
|
|
596
|
+
async extractToCache(packageId, version, packageData) {
|
|
597
|
+
const packageName = `${packageId}#${version}`;
|
|
598
|
+
const packagePath = path.join(this.cacheFolder, packageName);
|
|
599
|
+
|
|
600
|
+
// Ensure cache folder exists
|
|
601
|
+
await fs.mkdir(this.cacheFolder, { recursive: true });
|
|
602
|
+
|
|
603
|
+
// Create package folder
|
|
604
|
+
await fs.mkdir(packagePath, { recursive: true });
|
|
605
|
+
|
|
606
|
+
// Extract tar.gz
|
|
607
|
+
return new Promise((resolve, reject) => {
|
|
608
|
+
const gunzip = zlib.createGunzip();
|
|
609
|
+
const extract = tar.extract({
|
|
610
|
+
cwd: packagePath,
|
|
611
|
+
strict: true
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
gunzip.on('error', reject);
|
|
615
|
+
extract.on('error', reject);
|
|
616
|
+
extract.on('finish', () => resolve(packageName));
|
|
617
|
+
|
|
618
|
+
// Create a readable stream from the buffer and pipe through gunzip to tar
|
|
619
|
+
const stream = require('stream');
|
|
620
|
+
const bufferStream = new stream.PassThrough();
|
|
621
|
+
bufferStream.end(packageData);
|
|
622
|
+
|
|
623
|
+
bufferStream
|
|
624
|
+
.pipe(gunzip)
|
|
625
|
+
.pipe(extract);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
class PackageContentLoader {
|
|
631
|
+
/**
|
|
632
|
+
* @param {string} packageFolder - Path to the extracted NPM package folder
|
|
633
|
+
*/
|
|
634
|
+
constructor(packageFolder) {
|
|
635
|
+
this.packageFolder = packageFolder;
|
|
636
|
+
this.packageSubfolder = path.join(packageFolder, 'package');
|
|
637
|
+
this.indexPath = path.join(this.packageSubfolder, '.index.json');
|
|
638
|
+
this.index = null;
|
|
639
|
+
this.indexByTypeAndId = new Map();
|
|
640
|
+
this.indexByCanonical = new Map();
|
|
641
|
+
this.loaded = false;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Initialize the loader by reading and parsing the index
|
|
646
|
+
* @returns {Promise<void>}
|
|
647
|
+
*/
|
|
648
|
+
async initialize() {
|
|
649
|
+
if (this.loaded) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const packageSource = path.join(this.packageFolder, 'package', 'package.json');
|
|
654
|
+
const packageContent = await fs.readFile(packageSource, 'utf8');
|
|
655
|
+
this.package = JSON.parse(packageContent);
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const indexContent = await fs.readFile(this.indexPath, 'utf8');
|
|
659
|
+
this.index = JSON.parse(indexContent);
|
|
660
|
+
|
|
661
|
+
if (!this.index.files || !Array.isArray(this.index.files)) {
|
|
662
|
+
throw new Error('Invalid index file: missing or invalid files array');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Build lookup structures
|
|
666
|
+
this.buildIndexes();
|
|
667
|
+
this.loaded = true;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
throw new Error(`Failed to load package index from ${this.indexPath}: ${error.message}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Build internal indexes for efficient lookups
|
|
675
|
+
*/
|
|
676
|
+
buildIndexes() {
|
|
677
|
+
for (const entry of this.index.files) {
|
|
678
|
+
// Index by resourceType and id
|
|
679
|
+
if (entry.resourceType && entry.id) {
|
|
680
|
+
const key = `${entry.resourceType}/${entry.id}`;
|
|
681
|
+
this.indexByTypeAndId.set(key, entry);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Index by canonical URL (with and without version)
|
|
685
|
+
if (entry.url) {
|
|
686
|
+
// Index without version
|
|
687
|
+
this.indexByCanonical.set(entry.url, entry);
|
|
688
|
+
|
|
689
|
+
// Index with version if present
|
|
690
|
+
if (entry.version) {
|
|
691
|
+
const versionedUrl = `${entry.url}|${entry.version}`;
|
|
692
|
+
this.indexByCanonical.set(versionedUrl, entry);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Load a resource by reference
|
|
700
|
+
* @param {Object} reference - Reference object
|
|
701
|
+
* @param {string} [reference.resourceType] - Resource type
|
|
702
|
+
* @param {string} [reference.id] - Resource id
|
|
703
|
+
* @param {string} [reference.url] - Canonical URL
|
|
704
|
+
* @param {string} [reference.version] - Version (optional)
|
|
705
|
+
* @returns {Promise<Object|null>} Loaded resource or null if not found
|
|
706
|
+
*/
|
|
707
|
+
async loadByReference(reference) {
|
|
708
|
+
await this.initialize();
|
|
709
|
+
|
|
710
|
+
let entry = null;
|
|
711
|
+
|
|
712
|
+
// Try to find by resourceType and id
|
|
713
|
+
if (reference.resourceType && reference.id) {
|
|
714
|
+
const key = `${reference.resourceType}/${reference.id}`;
|
|
715
|
+
entry = this.indexByTypeAndId.get(key);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Try to find by canonical URL
|
|
719
|
+
if (!entry && reference.url) {
|
|
720
|
+
if (reference.version) {
|
|
721
|
+
// Try with version first
|
|
722
|
+
const versionedUrl = `${reference.url}|${reference.version}`;
|
|
723
|
+
entry = this.indexByCanonical.get(versionedUrl);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Try without version if not found
|
|
727
|
+
if (!entry) {
|
|
728
|
+
entry = this.indexByCanonical.get(reference.url);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!entry) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return await this.loadFile(entry);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Get a list of resources of a given type
|
|
741
|
+
* @param {string} resourceType - The resource type to filter by
|
|
742
|
+
* @returns {Promise<Array>} Array of index entries for the given type
|
|
743
|
+
*/
|
|
744
|
+
async getResourcesByType(resourceType) {
|
|
745
|
+
await this.initialize();
|
|
746
|
+
|
|
747
|
+
return this.index.files.filter(entry => entry.resourceType === resourceType);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Load all files that pass a given filter
|
|
752
|
+
* @param {Function} filterFn - Filter function that takes an index entry and returns boolean
|
|
753
|
+
* @returns {Promise<Array>} Array of loaded resources that pass the filter
|
|
754
|
+
*/
|
|
755
|
+
async loadByFilter(filterFn) {
|
|
756
|
+
await this.initialize();
|
|
757
|
+
|
|
758
|
+
const filteredEntries = this.index.files.filter(filterFn);
|
|
759
|
+
const loadPromises = filteredEntries.map(entry => this.loadFile(entry));
|
|
760
|
+
|
|
761
|
+
return await Promise.all(loadPromises);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Load a single file based on its index entry
|
|
766
|
+
* @param {Object} entry - Index entry
|
|
767
|
+
* @returns {Promise<Object>} Loaded resource
|
|
768
|
+
*/
|
|
769
|
+
async loadFile(entry) {
|
|
770
|
+
if (!entry.filename) {
|
|
771
|
+
throw new Error('Index entry missing filename');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const filePath = path.join(this.packageSubfolder, entry.filename);
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
778
|
+
return JSON.parse(content);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
throw new Error(`Failed to load file ${entry.filename}: ${error.message}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Get the raw index data
|
|
786
|
+
* @returns {Promise<Object>} The index object
|
|
787
|
+
*/
|
|
788
|
+
async getIndex() {
|
|
789
|
+
await this.initialize();
|
|
790
|
+
return this.index;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Get all resources (index entries only, not loaded)
|
|
795
|
+
* @returns {Promise<Array>} All index entries
|
|
796
|
+
*/
|
|
797
|
+
async getAllResources() {
|
|
798
|
+
await this.initialize();
|
|
799
|
+
return this.index.files;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Check if a resource exists by reference
|
|
804
|
+
* @param {Object} reference - Reference object (same as loadByReference)
|
|
805
|
+
* @returns {Promise<boolean>} True if resource exists
|
|
806
|
+
*/
|
|
807
|
+
async exists(reference) {
|
|
808
|
+
await this.initialize();
|
|
809
|
+
|
|
810
|
+
// Check by resourceType and id
|
|
811
|
+
if (reference.resourceType && reference.id) {
|
|
812
|
+
const key = `${reference.resourceType}/${reference.id}`;
|
|
813
|
+
if (this.indexByTypeAndId.has(key)) {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Check by canonical URL
|
|
819
|
+
if (reference.url) {
|
|
820
|
+
if (reference.version) {
|
|
821
|
+
const versionedUrl = `${reference.url}|${reference.version}`;
|
|
822
|
+
if (this.indexByCanonical.has(versionedUrl)) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (this.indexByCanonical.has(reference.url)) {
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Get statistics about the package content
|
|
837
|
+
* @returns {Promise<Object>} Statistics object
|
|
838
|
+
*/
|
|
839
|
+
async getStatistics() {
|
|
840
|
+
await this.initialize();
|
|
841
|
+
|
|
842
|
+
const stats = {
|
|
843
|
+
totalResources: this.index.files.length,
|
|
844
|
+
indexVersion: this.index['index-version'],
|
|
845
|
+
resourceTypes: {}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
for (const entry of this.index.files) {
|
|
849
|
+
if (entry.resourceType) {
|
|
850
|
+
stats.resourceTypes[entry.resourceType] =
|
|
851
|
+
(stats.resourceTypes[entry.resourceType] || 0) + 1;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return stats;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
fhirVersion() {
|
|
859
|
+
return this.package.fhirVersions[0];
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
id() {
|
|
863
|
+
return this.package.name;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
version() {
|
|
867
|
+
return this.package.version;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
pid() {
|
|
871
|
+
return this.id()+"#"+this.version();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
module.exports = { PackageManager, PackageContentLoader };
|