fhirsmith 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to Health Intersections FHIRsmith will be documented in this
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.16.1] - 2026-10-16
9
+
10
+ ### Added
11
+
12
+ - The dashboard always says which FHIRsmith release the server runs, and whether a newer one is available (looked up once a day with the npm audit check; before, the latest release was only looked up, and only mentioned, when the audit found something)
13
+ - README: how to follow releases (GitHub release emails, the releases Atom feed, and the dashboard)
14
+
15
+ ### Fixed
16
+
17
+ - Packages: SUSHI (and anything else using fhir-package-loader) couldn't download packages from this server: `GET /packages/{id}/{version}` returned the version's JSON document instead of the tarball (since 0.16.0). The rule was "JSON when Accept includes `application/json`", but fhir-package-loader downloads with axios's default `Accept: application/json, text/plain, */*`, and plenty of other HTTP clients send JSON alongside `*/*` by default without wanting it. The version document is now returned only when the request prefers JSON: a JSON type ranked above any `*/*`, `application/*` or tarball type by q-value (`application/json` alone, or `application/json, */*;q=0.1`). Everything else gets the tarball, and `FHIR-Package-Download` still forces it
18
+ - A POST with no Content-Type now says so (it gave "Unsupported Media Type: " with nothing after it)
19
+ - A form post whose Content-Type has a charset (`application/x-www-form-urlencoded; charset=UTF-8`) was refused as an unsupported media type
20
+ - Links in published IGs to a resource by the id it had in its source (`ValueSet/2.16.840.1.113762.1.4.1`, from before ids were prefixed, as in `ValueSet/vsac-2.16.840.1.113762.1.4.1`) work again in a browser: a read of an id nothing has redirects to the one resource that had that id in its source, or lists them when there are several. Other requests still get a 404
21
+
22
+ ### Tx Conformance Statement
23
+
24
+ FHIRsmith v0.16.1 passed all 3630 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed+mimetypes+icd-11+closure, tests v1.9.6, runner v7.0.2)
25
+
8
26
  ## [0.16.0] - 2026-10-15
9
27
 
10
28
  ### Security
package/README.md CHANGED
@@ -259,7 +259,7 @@ Each GitHub Release includes:
259
259
  - `ghcr.io/healthintersections/fhirsmith:latest`
260
260
  - `ghcr.io/healthintersections/fhirsmith:vX.Y.Z`
261
261
  - `ghcr.io/healthintersections/fhirsmith:X.Y.Z`
262
- - **npm package** published to npmjs.org as `fhirsmith` *(if you add this)*
262
+ - **npm package** published to npmjs.org as [`fhirsmith`](https://www.npmjs.com/package/fhirsmith)
263
263
 
264
264
  Release images carry OCI labels, so an image says which release it is without
265
265
  having to trust the tag it was pulled under:
@@ -271,6 +271,19 @@ docker inspect --format '{{json .Config.Labels}}' ghcr.io/healthintersections/fh
271
271
  `org.opencontainers.image.version` is the release, `.revision` the commit it was
272
272
  built from, and `.source` is what links the package to this repository on GHCR.
273
273
 
274
+ ### Tracking Releases
275
+
276
+ Server administrators should follow releases: a server is only as safe as the
277
+ release it runs.
278
+
279
+ - **Email**: on the [GitHub repository](https://github.com/HealthIntersections/FHIRsmith),
280
+ choose **Watch → Custom → Releases**. Each release is emailed with its notes (needs a GitHub account)
281
+ - **Atom feed**: `https://github.com/HealthIntersections/FHIRsmith/releases.atom` - each
282
+ entry carries the release notes, for a feed reader or a Slack/Teams feed integration
283
+ - **The server itself**: once a day the server looks up the latest release on npm, and the
284
+ dashboard says whether a newer one is available (this is part of the npm audit check, so
285
+ `npmAudit.enabled = false` turns it off too)
286
+
274
287
  ### CI Images
275
288
 
276
289
  Every push to main also publishes `:cibuild` and `:cibuild-<sha>` to the same
package/gr.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ git tag v$1
3
+ git push origin v$1
@@ -13,8 +13,9 @@
13
13
  // Config (all optional), top level of config.json:
14
14
  // "npmAudit": { "enabled": true, "intervalHours": 24 }
15
15
  //
16
- // Each check also looks up the latest fhirsmith release on npm, so the advice can say
17
- // whether upgrading will help. When this server is already on the latest release it won't:
16
+ // Each check also looks up the latest fhirsmith release on npm. The dashboard always says
17
+ // whether a newer release is available, and the advice about any findings says whether
18
+ // upgrading will help. When this server is already on the latest release it won't:
18
19
  // npm ignores our package.json "overrides" for anything but a clone + npm ci, so an
19
20
  // npm-installed server can carry transitive versions the repo itself has overridden away.
20
21
 
@@ -185,6 +186,41 @@ class NpmAudit {
185
186
  return true;
186
187
  }
187
188
 
189
+ /**
190
+ * This server's own version, from package.json (collectPackages reads it too, but that
191
+ * can fail for reasons that have nothing to do with the version)
192
+ */
193
+ readInstalledVersion() {
194
+ try {
195
+ const pkg = JSON.parse(fs.readFileSync(path.join(this.appDir, 'package.json'), 'utf8'));
196
+ if (pkg.version) {
197
+ this.installedVersion = pkg.version;
198
+ }
199
+ } catch (e) {
200
+ // leave it as it was
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Dashboard line: this server's version, and whether there is a newer release
206
+ */
207
+ renderRelease() {
208
+ if (!this.installedVersion) {
209
+ return '';
210
+ }
211
+ let html = `<p><strong>FHIRsmith release:</strong> this server runs ${escape(this.installedVersion)}`;
212
+ const cmp = compareVersions(this.installedVersion, this.latestVersion);
213
+ if (cmp === null) {
214
+ html += ' (the latest release could not be checked)';
215
+ } else if (cmp < 0) {
216
+ html += ` - <span style="color:#b60">FHIRsmith ${escape(this.latestVersion)} is available</span>` +
217
+ ' (<a href="https://github.com/HealthIntersections/FHIRsmith/releases">release notes</a>)';
218
+ } else {
219
+ html += ', the latest release';
220
+ }
221
+ return html + '</p>';
222
+ }
223
+
188
224
  /**
189
225
  * Look up the latest fhirsmith release. A failure only means the advice is less specific,
190
226
  * so it is logged and otherwise ignored (the previous answer is kept)
@@ -227,6 +263,9 @@ class NpmAudit {
227
263
  if (this.stats) {
228
264
  this.stats.task('npm audit', 'Checking');
229
265
  }
266
+ // independent of the audit: failures here are logged and otherwise ignored
267
+ this.readInstalledVersion();
268
+ await this.checkLatest();
230
269
  try {
231
270
  const packages = this.collectPackages();
232
271
  const response = await axios.post(this.url, packages, {
@@ -255,7 +294,6 @@ class NpmAudit {
255
294
  this.lastError = null;
256
295
  const summary = this.summary();
257
296
  if (findings.length > 0) {
258
- await this.checkLatest();
259
297
  if (this.logger) {
260
298
  this.logger.warn(`npm audit: ${summary} in ${this.packageCount} packages: ` +
261
299
  findings.map(f => `${f.name} (${f.severity}) ${f.url}`).join('; '));
@@ -356,6 +394,7 @@ class NpmAudit {
356
394
  if (!this.enabled) {
357
395
  return html + '<p><strong>npm audit:</strong> disabled (npmAudit.enabled = false)</p></div>';
358
396
  }
397
+ html += this.renderRelease();
359
398
  if (!this.checkedAt) {
360
399
  html += '<p><strong>npm audit:</strong> ';
361
400
  html += this.lastError ? `<span style="color:#b00">could not be run: ${escape(this.lastError)}</span>` : 'not run yet';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "txVersion": "1.9.6",
5
5
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
6
6
  "main": "server.js",
@@ -350,9 +350,12 @@ paths:
350
350
  summary: Download a package tarball, or get a version's document
351
351
  description: |
352
352
  Returns the package tarball (`.tgz`) - or, when the request's `Accept` header
353
- asks for `application/json` (or npm's `application/vnd.npm.install-v1+json`),
354
- the version's npm document: the same object as that version's entry in the
355
- package document. The response varies on `Accept` and `FHIR-Package-Download`.
353
+ prefers JSON, the version's npm document: the same object as that version's entry
354
+ in the package document. Preferring JSON means ranking `application/json` (or npm's
355
+ `application/vnd.npm.install-v1+json`) above any `*/*`, `application/*` or tarball
356
+ type by q-value: `application/json` alone does, but `application/json, text/plain, */*`
357
+ (axios's default, which SUSHI sends) does not. The response varies on `Accept` and
358
+ `FHIR-Package-Download`.
356
359
 
357
360
  A request with a `FHIR-Package-Download` header always gets the tarball. The
358
361
  HL7 Java tools send it; a client that downloads tarballs from this URL while
@@ -944,11 +944,13 @@ class PackagesModule {
944
944
  }
945
945
  }
946
946
 
947
- stopCrawlerJob() {
947
+ // node-cron 4: stop() may return a promise
948
+ async stopCrawlerJob() {
948
949
  if (this.crawlerJob) {
949
950
  this.crawler.shutdown();
950
- this.crawlerJob.stop();
951
+ const job = this.crawlerJob;
951
952
  this.crawlerJob = null;
953
+ await job.stop();
952
954
  pckLog.info('Package crawler job stopped');
953
955
  }
954
956
  }
@@ -1831,14 +1833,34 @@ class PackagesModule {
1831
1833
  res.send(htmlServer.renderPage('packages', 'npm Registry Check', content, stats));
1832
1834
  }
1833
1835
 
1834
- // GET /{id}/{version} is npm's version document when the request asks for JSON, and the
1835
- // tarball otherwise - which is what FHIR clients have always got there, without asking
1836
- // for JSON. A request carrying the TARBALL_HEADER always gets the tarball.
1836
+ // GET /{id}/{version} is npm's version document when the request prefers JSON, and the
1837
+ // tarball otherwise - which is what FHIR clients have always got there. Preferring JSON
1838
+ // means ranking it above anything else the request accepts: a JSON type with a higher q
1839
+ // than any */*, application/* or tarball type. Many clients send JSON together with */*
1840
+ // at the same q - axios's default, application/json, text/plain, */*, which is what
1841
+ // SUSHI (fhir-package-loader) sends when it downloads a tarball from here - and they get
1842
+ // the tarball. A request carrying the TARBALL_HEADER always gets the tarball.
1837
1843
  wantsVersionDocument(req) {
1838
1844
  if (req.get(TARBALL_HEADER) !== undefined) {
1839
1845
  return false;
1840
1846
  }
1841
- return /application\/(json|vnd\.npm\.install-v1\+json)/i.test(req.get('accept') || '');
1847
+ let json = 0;
1848
+ let other = 0;
1849
+ for (const part of String(req.get('accept') || '').split(',')) {
1850
+ const [type, ...params] = part.split(';').map(s => s.trim().toLowerCase());
1851
+ const qParam = params.find(p => p.startsWith('q='));
1852
+ const q = qParam ? parseFloat(qParam.substring(2)) : 1;
1853
+ if (!type || Number.isNaN(q)) {
1854
+ continue;
1855
+ }
1856
+ if (type === 'application/json' || type === 'application/vnd.npm.install-v1+json') {
1857
+ json = Math.max(json, q);
1858
+ } else if (['*/*', 'application/*', 'application/tar+gzip', 'application/gzip', 'application/x-gzip',
1859
+ 'application/x-tar', 'application/octet-stream'].includes(type)) {
1860
+ other = Math.max(other, q);
1861
+ }
1862
+ }
1863
+ return json > 0 && json > other;
1842
1864
  }
1843
1865
 
1844
1866
  async serveVersionDocument(secure, id, version, req, res) {
@@ -2844,7 +2866,7 @@ class PackagesModule {
2844
2866
  async shutdown() {
2845
2867
  pckLog.info('Shutting down Packages module...');
2846
2868
 
2847
- this.stopCrawlerJob();
2869
+ await this.stopCrawlerJob();
2848
2870
 
2849
2871
  // Close database connection
2850
2872
  if (this.db) {
@@ -31,7 +31,7 @@ In summary:
31
31
  | `GET /packages/catalog` | Search packages: `name`, `canonical`, `pkgcanonical`, `fhirversion`, `dependency`, `dependson`, `sort` |
32
32
  | `GET /packages/-/v1/search` | npm search API: `text`, `size`, `from`, plus the `/catalog` parameters |
33
33
  | `GET /packages/{id}` | npm package document: all versions, dependencies, tarball URLs |
34
- | `GET /packages/{id}/{version}` | Download a package tarball; or, when the request's `Accept` asks for JSON, npm's document for that version. A `FHIR-Package-Download` header always gets the tarball (the HL7 Java tools send it) |
34
+ | `GET /packages/{id}/{version}` | Download a package tarball; or, when the request's `Accept` prefers JSON (ranks it above `*/*`), npm's document for that version. A `FHIR-Package-Download` header always gets the tarball (the HL7 Java tools send it) |
35
35
  | `GET /packages/updates` | Versions published since a date |
36
36
  | `GET /packages/broken` | Package versions whose dependencies this registry doesn't hold |
37
37
  | `GET /packages/npm-check` | Check any registry against the npm registry protocol (a form, in a browser) |
package/shl/shl.js CHANGED
@@ -418,10 +418,12 @@ class SHLModule {
418
418
  }
419
419
  }
420
420
 
421
- stopCleanupJob() {
421
+ // node-cron 4: stop() may return a promise
422
+ async stopCleanupJob() {
422
423
  if (this.cleanupJob) {
423
- this.cleanupJob.stop();
424
+ const job = this.cleanupJob;
424
425
  this.cleanupJob = null;
426
+ await job.stop();
425
427
  }
426
428
  }
427
429
 
@@ -1060,7 +1062,7 @@ class SHLModule {
1060
1062
  async shutdown() {
1061
1063
  shlLog.info('Shutting down SHL module...');
1062
1064
 
1063
- this.stopCleanupJob();
1065
+ await this.stopCleanupJob();
1064
1066
 
1065
1067
  // Stop FHIR validator
1066
1068
  if (this.fhirValidator) {
@@ -58,6 +58,21 @@ function stripIdPrefix(id, spaceId) {
58
58
  return id.startsWith(prefix) && id.length > prefix.length ? id.substring(prefix.length) : null;
59
59
  }
60
60
 
61
+ /**
62
+ * The id a resource had in its source, for an id in any id space: everything after the
63
+ * prefix, which never contains a '-' (see checkIdPrefix). Null if the id has no prefix.
64
+ *
65
+ * @param {string} id
66
+ * @returns {string|null}
67
+ */
68
+ function originalId(id) {
69
+ if (typeof id !== 'string') {
70
+ return null;
71
+ }
72
+ const i = id.indexOf('-');
73
+ return i > 0 && i < id.length - 1 ? id.substring(i + 1) : null;
74
+ }
75
+
61
76
  /**
62
77
  * Whether a prefix given in the library YAML is acceptable. It's the start of an id, so it
63
78
  * may only use id characters, and no '-', so that the prefix of any id is unambiguous.
@@ -104,4 +119,4 @@ function assignIdSpace(provider, spaceId) {
104
119
 
105
120
  const CORE_PREFIX = 'core';
106
121
 
107
- module.exports = { applyIdPrefix, stripIdPrefix, checkIdPrefix, assignIdSpace, CORE_PREFIX };
122
+ module.exports = { applyIdPrefix, stripIdPrefix, originalId, checkIdPrefix, assignIdSpace, CORE_PREFIX };
package/tx/tx-html.js CHANGED
@@ -1329,6 +1329,30 @@ class TxHtmlRenderer {
1329
1329
  return html;
1330
1330
  }
1331
1331
 
1332
+ /**
1333
+ * The page for a read of an id that nothing has, but that several resources had in their
1334
+ * source (see ReadWorker.notFound): a list of them, to pick from.
1335
+ *
1336
+ * @param {string} resourceType
1337
+ * @param {string} id - the id that was asked for
1338
+ * @param {Array<{id, url, version, title, source}>} matches
1339
+ * @returns {string} html
1340
+ */
1341
+ renderIdChoices(resourceType, id, matches) {
1342
+ let html = `<p>There is no ${escape(resourceType)} with the id <code>${escape(id)}</code> on this server. ` +
1343
+ `Every resource here has an id that starts with a prefix for the source it came from, and ` +
1344
+ `${matches.length} ${escape(resourceType)} resources had the id <code>${escape(id)}</code> in their source:</p>`;
1345
+ html += '<table class="grid"><tr><th>Id</th><th>URL</th><th>Version</th><th>Title</th><th>Source</th></tr>';
1346
+ for (const m of matches) {
1347
+ // relative to /{type}/{id}, like the redirect when there is only one
1348
+ html += `<tr><td><a href="${escape(encodeURIComponent(m.id))}">${escape(m.id)}</a></td>` +
1349
+ `<td>${escape(m.url || '')}</td><td>${escape(m.version || '')}</td>` +
1350
+ `<td>${escape(m.title || '')}</td><td>${escape(m.source || '')}</td></tr>`;
1351
+ }
1352
+ html += '</table>';
1353
+ return html;
1354
+ }
1355
+
1332
1356
  /**
1333
1357
  * Render Bundle resource
1334
1358
  */
package/tx/tx.js CHANGED
@@ -516,8 +516,12 @@ class TXModule {
516
516
  return res.status(400).json(buildOperationOutcome('error', 'invalid', `Invalid XML: ${e.message}`));
517
517
  }
518
518
  }
519
- } else if (contentType != 'application/x-www-form-urlencoded') {
520
- return res.status(415).json(buildOperationOutcome('error', 'invalid', `Unsupported Media Type: ${contentType}`));
519
+ } else if (contentType.split(';')[0].trim().toLowerCase() != 'application/x-www-form-urlencoded') {
520
+ // compare the media type alone: a form post may carry a charset parameter
521
+ const message = contentType
522
+ ? `Unsupported Media Type: ${contentType}`
523
+ : `No Content-Type was given for this ${req.method}; use application/fhir+json or application/fhir+xml`;
524
+ return res.status(415).json(buildOperationOutcome('error', 'invalid', message));
521
525
  }
522
526
 
523
527
  if (req.body) {
@@ -994,7 +998,7 @@ class TXModule {
994
998
  ));
995
999
  }
996
1000
  let worker = new ReadWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
997
- await worker.handle(req, res, resourceType);
1001
+ await worker.handle(req, res, resourceType, (req, res, resourceType, id, matches) => this.renderIdChoices(req, res, resourceType, id, matches));
998
1002
  } finally {
999
1003
  this.countRequest(endpointPath, 'read', Date.now() - start);
1000
1004
  }
@@ -1432,6 +1436,17 @@ class TXModule {
1432
1436
  return count;
1433
1437
  }
1434
1438
 
1439
+ // A read of an unprefixed id that several resources had in their source: the page
1440
+ // that lists them (status 300, Multiple Choices). See ReadWorker.notFound.
1441
+ async renderIdChoices(req, res, resourceType, id, matches) {
1442
+ const txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txEndpoint.provider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
1443
+ const content = txhtml.renderIdChoices(resourceType, id, matches);
1444
+ const html = await txhtml.renderPage(`${resourceType}/${id}`, content, req.txEndpoint, req.txStartTime);
1445
+ res.status(300);
1446
+ res.setHeader('Content-Type', 'text/html');
1447
+ return res.send(html);
1448
+ }
1449
+
1435
1450
  // $closure usage for the home page: null when closure isn't turned on
1436
1451
  closureStats() {
1437
1452
  if (!this.closureStore) {
@@ -8,6 +8,9 @@ const { TerminologyWorker } = require('./worker');
8
8
  const {debugLog} = require("../operation-context");
9
9
 
10
10
  const { buildOperationOutcome } = require('../library/operation-outcome');
11
+ const { originalId } = require('../library/resource-ids');
12
+ const { acceptsHtml } = require('../tx-html');
13
+
11
14
  class ReadWorker extends TerminologyWorker {
12
15
  /**
13
16
  * @param {OperationContext} opContext - Operation context
@@ -32,10 +35,12 @@ class ReadWorker extends TerminologyWorker {
32
35
  * @param {express.Request} req - Express request (with txProvider attached)
33
36
  * @param {express.Response} res - Express response
34
37
  * @param {string} resourceType - The resource type (CodeSystem, ValueSet, ConceptMap)
35
- * @param {Object} log - Logger instance
38
+ * @param {Function} [renderChoices] - (req, res, resourceType, id, matches) renders the
39
+ * page that lists the resources an unprefixed id could mean (see notFound)
36
40
  */
37
- async handle(req, res, resourceType) {
41
+ async handle(req, res, resourceType, renderChoices = null) {
38
42
  const { id } = req.params;
43
+ this.renderChoices = renderChoices;
39
44
 
40
45
  this.log.debug(`Read ${resourceType}/${id}`);
41
46
 
@@ -114,7 +119,7 @@ class ReadWorker extends TerminologyWorker {
114
119
  }
115
120
  }
116
121
 
117
- return res.status(404).json(buildOperationOutcome('error', 'not-found', `CodeSystem/${id} not found`));
122
+ return await this.notFound(req, res, 'CodeSystem', id);
118
123
  }
119
124
 
120
125
  /**
@@ -131,7 +136,7 @@ class ReadWorker extends TerminologyWorker {
131
136
  }
132
137
  }
133
138
 
134
- return res.status(404).json(buildOperationOutcome('error', 'not-found', `ValueSet/${id} not found`));
139
+ return await this.notFound(req, res, 'ValueSet', id);
135
140
  }
136
141
  /**
137
142
  * Handle ConceptMap read
@@ -147,7 +152,84 @@ class ReadWorker extends TerminologyWorker {
147
152
  }
148
153
  }
149
154
 
150
- return res.status(404).json(buildOperationOutcome('error', 'not-found', `ConceptMap/${id} not found`));
155
+ return await this.notFound(req, res, 'ConceptMap', id);
156
+ }
157
+
158
+ /**
159
+ * Nothing has the id asked for. Published IGs link to resources by the id they had in
160
+ * their source (ValueSet/2.16.840.1.113762.1.4.1), from before every id was put in its
161
+ * provider's id space (ValueSet/vsac-2.16.840.1.113762.1.4.1). So for a browser, look
162
+ * for resources that had that id in their source: if there's one, redirect to it, and
163
+ * if there are several, list them. Anything other than a browser still gets a 404 -
164
+ * an unprefixed id can't say which resource it means (see library/resource-ids.js).
165
+ */
166
+ async notFound(req, res, resourceType, id) {
167
+ if (this.renderChoices && acceptsHtml(req)) {
168
+ const matches = await this.findByOriginalId(resourceType, id);
169
+ if (matches.length === 1) {
170
+ req.logInfo = `redirected to ${matches[0].id}`;
171
+ // relative, so that it works whatever path the server is behind; keeps the query
172
+ const q = req.originalUrl.indexOf('?');
173
+ return res.redirect(302, encodeURIComponent(matches[0].id) + (q >= 0 ? req.originalUrl.substring(q) : ''));
174
+ }
175
+ if (matches.length > 1) {
176
+ req.logInfo = `${matches.length} matches`;
177
+ return await this.renderChoices(req, res, resourceType, id, matches);
178
+ }
179
+ }
180
+ return res.status(404).json(buildOperationOutcome('error', 'not-found', `${resourceType}/${id} not found`));
181
+ }
182
+
183
+ /**
184
+ * The resources of a type that had this id in their source, across all the providers.
185
+ *
186
+ * @returns {Promise<Array<{id, url, version, title, source}>>} in provider order
187
+ */
188
+ async findByOriginalId(resourceType, id) {
189
+ const found = [];
190
+ const add = (resource) => {
191
+ const json = resource.jsonObj || resource;
192
+ if (json.id && !found.some(m => m.id === json.id)) {
193
+ found.push({
194
+ id: json.id,
195
+ url: json.url || null,
196
+ version: json.version || null,
197
+ title: json.title || json.name || null,
198
+ source: typeof resource.sourcePackage === 'string' ? resource.sourcePackage : null
199
+ });
200
+ }
201
+ };
202
+
203
+ switch (resourceType) {
204
+ case 'CodeSystem':
205
+ // the map holds a code system under more than one key (url, url|version)
206
+ for (const cs of new Set(this.provider.codeSystems.values())) {
207
+ this.deadCheck('findByOriginalId-cs');
208
+ if (originalId(cs.jsonObj.id) === id) {
209
+ add(cs);
210
+ }
211
+ }
212
+ break;
213
+ case 'ValueSet':
214
+ for (const vsp of this.provider.valueSetProviders) {
215
+ this.deadCheck('findByOriginalId-vs');
216
+ const vs = vsp.spaceId ? await vsp.fetchValueSetById(`${vsp.spaceId}-${id}`) : null;
217
+ if (vs) {
218
+ add(vs);
219
+ }
220
+ }
221
+ break;
222
+ case 'ConceptMap':
223
+ for (const cmp of this.provider.conceptMapProviders) {
224
+ this.deadCheck('findByOriginalId-cm');
225
+ const cm = cmp.spaceId ? await cmp.fetchConceptMapById(`${cmp.spaceId}-${id}`) : null;
226
+ if (cm) {
227
+ add(cm);
228
+ }
229
+ }
230
+ break;
231
+ }
232
+ return found;
151
233
  }
152
234
  }
153
235