fhirsmith 0.14.2 → 0.15.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/config-template.json +10 -0
  3. package/library/changelog.js +57 -0
  4. package/library/npm-audit.js +62 -9
  5. package/package.json +2 -2
  6. package/packages/npm-check-page.js +108 -0
  7. package/packages/npm-check.js +450 -0
  8. package/packages/openapi.yaml +158 -5
  9. package/packages/package-crawler.js +203 -20
  10. package/packages/packages.js +250 -8
  11. package/packages/readme.md +55 -2
  12. package/packages/utilities/dedupe-versions.js +242 -0
  13. package/registry/crawler.js +17 -0
  14. package/registry/fhirsmith-releases.js +3 -6
  15. package/registry/model.js +3 -0
  16. package/registry/registry.js +28 -3
  17. package/translations/Messages.properties +3 -0
  18. package/tx/README.md +20 -4
  19. package/tx/cm/cm-api.js +25 -10
  20. package/tx/cm/cm-database.js +0 -4
  21. package/tx/cm/cm-package.js +53 -14
  22. package/tx/cs/cs-api.js +6 -1
  23. package/tx/cs/cs-icd11.js +23 -6
  24. package/tx/cs/cs-provider-api.js +22 -7
  25. package/tx/cs/cs-provider-list.js +10 -9
  26. package/tx/cs/icd11-expressions.js +5 -6
  27. package/tx/importers/icd11-schema.md +1 -0
  28. package/tx/importers/import-icd11.js +39 -0
  29. package/tx/library/resource-ids.js +107 -0
  30. package/tx/library.js +96 -24
  31. package/tx/ocl/cm-ocl.cjs +23 -15
  32. package/tx/ocl/cs-ocl.cjs +9 -10
  33. package/tx/ocl/vs-ocl.cjs +17 -17
  34. package/tx/provider.js +80 -13
  35. package/tx/tx.fhir.org.yml +17 -9
  36. package/tx/tx.js +7 -1
  37. package/tx/vs/vs-api.js +25 -10
  38. package/tx/vs/vs-database.js +0 -4
  39. package/tx/vs/vs-package.js +48 -31
  40. package/tx/vs/vs-vsac.js +20 -5
  41. package/tx/workers/lookup.js +7 -4
  42. package/tx/workers/metadata.js +2 -1
  43. package/tx/workers/read.js +3 -18
package/tx/cs/cs-icd11.js CHANGED
@@ -113,8 +113,11 @@ class ICD11Services extends BaseCSServices {
113
113
  /**
114
114
  * The default is system|version, but $lookup's `name` is meant to be a display name for
115
115
  * the code system. OMOP overrides it the same way; WHO returns the linearization title.
116
+ * It follows the request's languages like any other display, falling back to the primary
117
+ * title. $lookup passes its working languages (displayLanguage, else Accept-Language),
118
+ * since displayLanguage never reaches opContext.langs; other callers pass nothing.
116
119
  */
117
- name() { return this.shared.title; }
120
+ name(languages) { return this.shared.titles?.[this.#displayLang(languages)] || this.shared.title; }
118
121
  description() { return this.shared.title; }
119
122
  totalCount() { return this.shared.count; }
120
123
  defLang() { return 'en'; }
@@ -191,12 +194,13 @@ class ICD11Services extends BaseCSServices {
191
194
  return row ? new ICD11Concept(row) : null;
192
195
  }
193
196
 
194
- /** The three things the expression parser needs from the database. */
197
+ /** What the expression parser needs: the database, and the request's translations. */
195
198
  #expressionContext() {
196
199
  return {
197
200
  get: (sql, params) => this.#get(sql, params),
198
201
  all: (sql, params) => this.#all(sql, params),
199
202
  resolve: (text) => this.#resolve(text),
203
+ translate: (msgId, params) => this.opContext.i18n.translate(msgId, this.opContext.langs, params),
200
204
  };
201
205
  }
202
206
 
@@ -309,8 +313,8 @@ class ICD11Services extends BaseCSServices {
309
313
  * iterable of Language, in quality order; the database holds translations keyed on the
310
314
  * primary subtag, so that is what is matched.
311
315
  */
312
- #displayLang() {
313
- const langs = this.opContext?.langs;
316
+ #displayLang(languages) {
317
+ const langs = languages || this.opContext?.langs;
314
318
  if (langs) {
315
319
  for (const l of langs) {
316
320
  const base = l.language || String(l.code || '').split('-')[0];
@@ -615,8 +619,9 @@ class ICD11Services extends BaseCSServices {
615
619
  if (!target) {
616
620
  // a value set that names a code that is not there is broken, and saying so is more
617
621
  // use than quietly expanding to nothing
618
- throw new Issue('error', 'code-invalid', null, null,
619
- `The filter value '${value}' is not a code in ${this.system()}`, 'invalid-code', 422);
622
+ throw new Issue('error', 'code-invalid', null, 'FILTER_VALUE_UNKNOWN_CODE',
623
+ this.opContext.i18n.translate('FILTER_VALUE_UNKNOWN_CODE', this.opContext.langs, [value, this.system()]),
624
+ 'invalid-code', 422);
620
625
  }
621
626
  filterContext.filters.push(new ICD11FilterSet('isa', {
622
627
  ancestorId: target.id, minDepth: op === 'is-a' ? 0 : 1, value,
@@ -749,12 +754,24 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
749
754
  }
750
755
  const count = await get('SELECT COUNT(*) n FROM concept WHERE system = ?', [sys.id]);
751
756
  const langs = await get("SELECT value FROM meta WHERE key = 'languages'", []);
757
+ // per-language titles; a database built before system_title existed has none
758
+ const all = (sql, p) => new Promise((res, rej) =>
759
+ db.all(sql, p, (e, r) => e ? rej(e) : res(r || [])));
760
+ const titles = {};
761
+ const hasTitles = await get(
762
+ "SELECT 1 x FROM sqlite_master WHERE type = 'table' AND name = 'system_title'", []);
763
+ if (hasTitles) {
764
+ for (const r of await all('SELECT lang, title FROM system_title WHERE system = ?', [sys.id])) {
765
+ titles[r.lang] = r.title;
766
+ }
767
+ }
752
768
  this._shared = {
753
769
  systemCode: sys.code,
754
770
  systemId: sys.id,
755
771
  url: sys.url,
756
772
  version: sys.version,
757
773
  title: sys.title,
774
+ titles,
758
775
  isTree: !!sys.is_tree,
759
776
  count: count.n,
760
777
  languages: (langs?.value || 'en').split(','),
@@ -1,22 +1,37 @@
1
+ const { assignIdSpace } = require('../library/resource-ids');
2
+
1
3
  /**
2
4
  * Abstract base class for value set providers
3
5
  * Defines the interface that all value set providers must implement
4
6
  */
5
7
  class AbstractCodeSystemProvider {
6
8
  /**
7
- * {int} Unique number assigned to this provider
9
+ * {string} The prefix of this provider's id space (set by assignIds) - given for its source in the library
10
+ * YAML, or autonumbered. Every code system this provider provides has an id that starts
11
+ * with spaceId + '-' (see tx/library/resource-ids.js)
8
12
  */
9
13
  spaceId;
10
14
 
11
15
  /**
12
- * ensure that the ids on the code systems are unique, if they are
13
- * in the global namespace
16
+ * Put this provider in an id space: set spaceId, and prefix the ids of the code systems it
17
+ * provides with spaceId + '-'. A provider is put in one id space, once - asking for a
18
+ * different one later is an error (see assignIdSpace)
14
19
  *
15
- * @param {Set<String>} ids
20
+ * @param {string} spaceId
16
21
  */
17
- // eslint-disable-next-line no-unused-vars
18
- assignIds(ids) {
19
- throw new Error('assignIds must be implemented by subclass');
22
+ assignIds(spaceId) {
23
+ if (assignIdSpace(this, spaceId)) {
24
+ this.prefixIds();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Prefix the ids of the code systems this provider provides with spaceId + '-', once spaceId
30
+ * is set (by assignIds). Code systems the provider provides later (see
31
+ * getCodeSystemChanges) must be given ids in the same space.
32
+ */
33
+ prefixIds() {
34
+ throw new Error('prefixIds must be implemented by subclass');
20
35
  }
21
36
 
22
37
  /**
@@ -1,4 +1,5 @@
1
1
  const { AbstractCodeSystemProvider } = require('./cs-provider-api');
2
+ const { applyIdPrefix } = require('../library/resource-ids');
2
3
 
3
4
  /**
4
5
  * Package-based ValueSet provider using shared database layer
@@ -11,18 +12,18 @@ class ListCodeSystemProvider extends AbstractCodeSystemProvider {
11
12
  codeSystems = [];
12
13
 
13
14
  /**
14
- * ensure that the ids on the code systems are unique, if they are
15
- * in the global namespace
16
- *
17
- * @param {Set<String>} ids
15
+ * prefix the ids of the code systems with this provider's spaceId. A code system without
16
+ * an id gets its position in the list
18
17
  */
19
- // eslint-disable-next-line no-unused-vars
20
- assignIds(ids) {
18
+ prefixIds() {
19
+ let i = 0;
21
20
  for (const cs of this.codeSystems) {
22
- if (!cs.id || ids.has("CodeSystem/"+cs.id)) {
23
- cs.id = ""+ids.size;
21
+ i++;
22
+ const json = cs.jsonObj || cs;
23
+ if (!json.id) {
24
+ json.id = String(i);
24
25
  }
25
- ids.add("CodeSystem/"+cs.id);
26
+ applyIdPrefix(cs, this.spaceId);
26
27
  }
27
28
  }
28
29
 
@@ -232,12 +232,11 @@ async function parseExpression(ctx, text) {
232
232
  */
233
233
  function notOnAnyAxis(ctx, stem, token) {
234
234
  const axes = stem.axes.map(a => a.axis_name).join(', ');
235
- const msg = axes
236
- ? `'${token.text}' is not a permitted value on any postcoordination axis of `
237
- + `'${stem.stem.text}'. That stem declares: ${axes}`
238
- : `'${token.text}' cannot be postcoordinated onto '${stem.stem.text}': that concept `
239
- + `declares no postcoordination axes`;
240
- return new Issue('error', 'code-invalid', null, null, msg, 'invalid-code', 400).handleAsOO(400);
235
+ const [msgId, params] = axes
236
+ ? ['ICD11_PC_VALUE_NOT_ON_AXIS', [token.text, stem.stem.text, axes]]
237
+ : ['ICD11_PC_NO_AXES', [token.text, stem.stem.text]];
238
+ return new Issue('error', 'code-invalid', null, msgId, ctx.translate(msgId, params),
239
+ 'invalid-code', 400).handleAsOO(400);
241
240
  }
242
241
 
243
242
  module.exports = { ICD11Expression, ExpressionToken, parseExpression, tokenize, SCHEMA_NS };
@@ -42,6 +42,7 @@ text lives in `designation`. The untouched native JSON for each concept is kept
42
42
  |---|---|
43
43
  | `meta` | key/value: release, languages, endpoints, tool version, request count |
44
44
  | `system` | one row per code system — `mms`, `icf`, `foundation` — with its canonical url, release and `is_tree` |
45
+ | `system_title` | the code system's title per language (`system.title` is the primary one); only titles WHO actually translated |
45
46
  | `concept` | one row per entity |
46
47
  | `concept_parent` | parent/child edges, `seq` preserving the API's order |
47
48
  | `concept_closure` | transitive closure including the reflexive row, so is-a never needs recursion |
@@ -55,6 +55,15 @@ CREATE TABLE IF NOT EXISTS system (
55
55
  uri_stem TEXT NOT NULL -- versioned uri prefix used by the native API
56
56
  );
57
57
 
58
+ -- the code system's own title, per language crawled (system.title is the primary language).
59
+ -- Only a title WHO actually translated is recorded -- an English fallback is not a French title.
60
+ CREATE TABLE IF NOT EXISTS system_title (
61
+ system INTEGER NOT NULL REFERENCES system(id),
62
+ lang TEXT NOT NULL,
63
+ title TEXT NOT NULL,
64
+ PRIMARY KEY (system, lang)
65
+ );
66
+
58
67
  -- one row per concept
59
68
  CREATE TABLE IF NOT EXISTS concept (
60
69
  id INTEGER PRIMARY KEY,
@@ -374,6 +383,8 @@ class Icd11Importer {
374
383
  setMeta: db.prepare(`INSERT INTO meta(key,value) VALUES (?,?)
375
384
  ON CONFLICT(key) DO UPDATE SET value=excluded.value`),
376
385
  getMeta: db.prepare(`SELECT value FROM meta WHERE key=?`),
386
+ insSystemTitle: db.prepare(`INSERT INTO system_title(system,lang,title) VALUES (?,?,?)
387
+ ON CONFLICT(system,lang) DO UPDATE SET title=excluded.title`),
377
388
  insSystem: db.prepare(`INSERT INTO system(code,url,version,title,is_tree,api_root,uri_stem)
378
389
  VALUES (@code,@url,@version,@title,@is_tree,@api_root,@uri_stem)
379
390
  ON CONFLICT(code) DO UPDATE SET
@@ -566,6 +577,8 @@ class Icd11Importer {
566
577
 
567
578
  for (const lang of languages) {
568
579
  const isPrimary = lang === primaryLang;
580
+ // before the resume check: a language whose concepts are all done still needs its title
581
+ await this.storeSystemTitle(systemId, def, lang, isPrimary ? root : null);
569
582
  const done = new Set(this.stmt.doneLangs.all(systemId, lang).map(r => r.uri));
570
583
  const todo = uris.filter(u => !done.has(unversion(u)));
571
584
  if (!todo.length) { this.log(` [${lang}] already complete`); continue; }
@@ -609,6 +622,32 @@ class Icd11Importer {
609
622
  return systemId;
610
623
  }
611
624
 
625
+ /**
626
+ * Record the linearization root's title in one language. `root` is the document already
627
+ * fetched for the primary language; other languages cost one extra call each. The root's
628
+ * title carries the language WHO actually answered in, so an untranslated title (WHO falls
629
+ * back to English) is skipped rather than stored under the requested language.
630
+ */
631
+ async storeSystemTitle(systemId, def, lang, root) {
632
+ if (!root) {
633
+ const r = await this.getEntity(WHO + def.apiRoot, lang);
634
+ if (!r.ok) {
635
+ this.problem('title-failed', r.error, { system: def.code, lang, uri: def.apiRoot });
636
+ this.warn(` ! could not read the ${lang} title of ${def.apiRoot}: ${r.error}`);
637
+ return;
638
+ }
639
+ root = r.body;
640
+ }
641
+ const title = textOf(root.title);
642
+ if (!title) return;
643
+ const got = langOf(root.title, lang);
644
+ if (got.split('-')[0] !== lang.split('-')[0]) {
645
+ this.log(` [${lang}] no translated title (WHO answered in ${got})`);
646
+ return;
647
+ }
648
+ this.stmt.insSystemTitle.run(systemId, lang, title);
649
+ }
650
+
612
651
  /**
613
652
  * Turn the parent and scale-entity uris collected during the crawl into real ids.
614
653
  * `rootUri` is the linearization root, which the top-level concepts name as their
@@ -0,0 +1,107 @@
1
+ //
2
+ // Resource ids in the server's id spaces
3
+ //
4
+ // Every CodeSystem, ValueSet and ConceptMap the server serves by id lives in the id space of
5
+ // the provider that loaded it, and its id is the provider's prefix, a '-', then the id the
6
+ // resource had in its source: "tho-v3-ActCode", "core-v3-MaritalStatus". The prefix is the
7
+ // provider's spaceId: given in the library YAML (npm:hl7.terminology=tho), autonumbered by
8
+ // the library when the YAML doesn't give one, and "core" for the FHIR core package each
9
+ // endpoint loads for itself.
10
+ //
11
+ // No resource is served under its original id. Two sources can carry resources with the same
12
+ // id (hl7.terminology and hl7.fhir.r4.core both have a CodeSystem v3-MaritalStatus), so an
13
+ // unprefixed id can't say which one it means - and anything that looks a resource up by its
14
+ // raw id must fail, rather than find whichever one happens to come first.
15
+ //
16
+
17
+ // resources whose id has been prefixed - so prefixing is idempotent, and a raw id that
18
+ // happens to start with the prefix still gets one
19
+ const prefixed = new WeakSet();
20
+
21
+ /**
22
+ * Prefix a resource's id with spaceId. The resource is either a wrapper with a jsonObj
23
+ * (CodeSystem, ValueSet, ConceptMap) or a plain json object. Does nothing to a resource
24
+ * that has already been prefixed, or that has no id.
25
+ *
26
+ * @param {Object} resource
27
+ * @param {string} spaceId
28
+ */
29
+ function applyIdPrefix(resource, spaceId) {
30
+ if (!resource || !spaceId || prefixed.has(resource)) {
31
+ return;
32
+ }
33
+ const json = resource.jsonObj || resource;
34
+ if (!json.id) {
35
+ return;
36
+ }
37
+ const id = `${spaceId}-${json.id}`;
38
+ json.id = id;
39
+ if (resource.jsonObj && Object.prototype.hasOwnProperty.call(resource, 'id')) {
40
+ resource.id = id;
41
+ }
42
+ prefixed.add(resource);
43
+ }
44
+
45
+ /**
46
+ * The id a resource had in its source, for an id served in spaceId - or null, if the id
47
+ * isn't in that space (including when it's a raw id, or there's no spaceId).
48
+ *
49
+ * @param {string} id
50
+ * @param {string} spaceId
51
+ * @returns {string|null}
52
+ */
53
+ function stripIdPrefix(id, spaceId) {
54
+ if (!spaceId || typeof id !== 'string') {
55
+ return null;
56
+ }
57
+ const prefix = `${spaceId}-`;
58
+ return id.startsWith(prefix) && id.length > prefix.length ? id.substring(prefix.length) : null;
59
+ }
60
+
61
+ /**
62
+ * Whether a prefix given in the library YAML is acceptable. It's the start of an id, so it
63
+ * may only use id characters, and no '-', so that the prefix of any id is unambiguous.
64
+ * 'core' is the endpoints' own core package, and 'x' is the code system factories
65
+ * (CodeSystem/x-...), so neither is available.
66
+ *
67
+ * @param {string} prefix
68
+ * @returns {string|null} why the prefix isn't acceptable, or null if it is
69
+ */
70
+ function checkIdPrefix(prefix) {
71
+ if (!/^[A-Za-z0-9.]{1,32}$/.test(prefix)) {
72
+ return `an id prefix may only contain letters, digits and '.' (up to 32 characters)`;
73
+ }
74
+ if (prefix === CORE_PREFIX || prefix === 'x') {
75
+ return `the id prefix '${prefix}' is reserved`;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Put a provider in an id space - the shared part of the providers' assignIds(spaceId).
82
+ * A provider is put in one id space, once: its resources are prefixed when it is, so
83
+ * moving it to another would leave them in the old one. Asking again for the same space
84
+ * does nothing.
85
+ *
86
+ * @param {Object} provider - a code system, value set or concept map provider
87
+ * @param {string} spaceId
88
+ * @returns {boolean} true if the provider has just been put in the space, and must now
89
+ * prefix its resources' ids
90
+ */
91
+ function assignIdSpace(provider, spaceId) {
92
+ if (!spaceId) {
93
+ throw new Error(`${provider.constructor.name}: no id space given`);
94
+ }
95
+ if (provider.spaceId) {
96
+ if (provider.spaceId === spaceId) {
97
+ return false;
98
+ }
99
+ throw new Error(`${provider.constructor.name} is already in the id space '${provider.spaceId}', so it can't be put in '${spaceId}'`);
100
+ }
101
+ provider.spaceId = spaceId;
102
+ return true;
103
+ }
104
+
105
+ const CORE_PREFIX = 'core';
106
+
107
+ module.exports = { applyIdPrefix, stripIdPrefix, checkIdPrefix, assignIdSpace, CORE_PREFIX };
package/tx/library.js CHANGED
@@ -39,6 +39,7 @@ const { OCLValueSetProvider } = require('./ocl/vs-ocl');
39
39
  const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
40
40
  const {UriServicesFactory} = require("./cs/cs-uri");
41
41
  const {debugLog} = require("./operation-context");
42
+ const { checkIdPrefix } = require("./library/resource-ids");
42
43
 
43
44
  /**
44
45
  * This class holds all the loaded content ready for processing
@@ -72,6 +73,17 @@ class Library {
72
73
  packageSources = [];
73
74
  externalSources = [];
74
75
 
76
+ /**
77
+ * {Map<Object, string>} the id prefix the library YAML gives for the source a provider was
78
+ * loaded from (npm:hl7.terminology=tho), by provider
79
+ */
80
+ #sourcePrefixes = new Map();
81
+
82
+ /**
83
+ * {Set<string>} every id prefix the library YAML gives - autonumbered prefixes skip them
84
+ */
85
+ #givenPrefixes = new Set();
86
+
75
87
  baseUrl = null;
76
88
  cacheFolder = null;
77
89
  startTime = Date.now();
@@ -171,6 +183,8 @@ class Library {
171
183
 
172
184
  this.log.info('Fetching Data from '+this.baseUrl);
173
185
 
186
+ this.#checkSourcePrefixes(config.sources);
187
+
174
188
  for (const source of config.sources) {
175
189
  try {
176
190
  await this.processSource(source, this.packageManager, "fetch");
@@ -223,15 +237,20 @@ class Library {
223
237
  this.assignIds();
224
238
  }
225
239
 
226
- async processSource(source, packageManager, mode) {
227
- // Parse the source string
240
+ /**
241
+ * Parse a source entry from the library YAML: type[!]:details[=prefix]
242
+ *
243
+ * @param {string} source
244
+ * @returns {{type: string, details: string, isDefault: boolean, prefix: string|null}}
245
+ */
246
+ #parseSource(source) {
228
247
  const colonIndex = source.indexOf(':');
229
248
  if (colonIndex === -1) {
230
249
  throw new Error(`Invalid source format: ${source}`);
231
250
  }
232
251
 
233
252
  let type = source.substring(0, colonIndex);
234
- const details = source.substring(colonIndex + 1);
253
+ let details = source.substring(colonIndex + 1);
235
254
 
236
255
  // Handle special markers (like ! for default)
237
256
  let isDefault = false;
@@ -240,6 +259,59 @@ class Library {
240
259
  isDefault = true;
241
260
  }
242
261
 
262
+ // the id prefix for the resources the source provides: everything after the last '='
263
+ let prefix = null;
264
+ const eqIndex = details.lastIndexOf('=');
265
+ if (eqIndex > -1) {
266
+ prefix = details.substring(eqIndex + 1);
267
+ details = details.substring(0, eqIndex);
268
+ const problem = checkIdPrefix(prefix);
269
+ if (problem) {
270
+ throw new Error(`Invalid source '${source}': ${problem}`);
271
+ }
272
+ }
273
+ return { type, details, isDefault, prefix };
274
+ }
275
+
276
+ /**
277
+ * Check the id prefixes the library YAML gives before anything is loaded - each id space
278
+ * must belong to one source
279
+ *
280
+ * @param {string[]} sources
281
+ */
282
+ #checkSourcePrefixes(sources) {
283
+ const seen = new Map();
284
+ for (const source of sources) {
285
+ const { prefix } = this.#parseSource(source);
286
+ if (prefix) {
287
+ if (seen.has(prefix)) {
288
+ throw new Error(`The id prefix '${prefix}' is given for both '${seen.get(prefix)}' and '${source}'`);
289
+ }
290
+ seen.set(prefix, source);
291
+ }
292
+ }
293
+ }
294
+
295
+ async processSource(source, packageManager, mode) {
296
+ const { type, details, isDefault, prefix } = this.#parseSource(source);
297
+
298
+ // the providers this source adds, so they can be given its id prefix
299
+ const csCount = this.codeSystemProviders.length;
300
+ const vsCount = this.valueSetProviders.length;
301
+ const cmCount = this.conceptMapProviders.length;
302
+
303
+ await this.#loadSource(type, details, isDefault, mode, packageManager);
304
+
305
+ if (prefix) {
306
+ this.#givenPrefixes.add(prefix);
307
+ for (const provider of [...this.codeSystemProviders.slice(csCount),
308
+ ...this.valueSetProviders.slice(vsCount), ...this.conceptMapProviders.slice(cmCount)]) {
309
+ this.#sourcePrefixes.set(provider, prefix);
310
+ }
311
+ }
312
+ }
313
+
314
+ async #loadSource(type, details, isDefault, mode, packageManager) {
243
315
  // Switch statement for different source types
244
316
  switch (type) {
245
317
  case 'internal':
@@ -1000,6 +1072,7 @@ class Library {
1000
1072
  // Now add the existing value set providers after the FHIR core packages
1001
1073
  provider.valueSetProviders.push(...this.valueSetProviders);
1002
1074
  provider.conceptMapProviders.push(...this.conceptMapProviders);
1075
+ provider.applyTHOPrecedence();
1003
1076
 
1004
1077
  // bind UCUM common value set
1005
1078
  let ucum = provider.codeSystemFactories.get("http://unitsofmeasure.org");
@@ -1030,30 +1103,29 @@ class Library {
1030
1103
  }
1031
1104
 
1032
1105
  /**
1033
- * all the loaded resources must have unique IDs for the get operation
1034
- * they must be assigned by the library on loading. providers can either assign
1035
- * ids from the global space at start up, or, if they can provide new resources
1036
- * later in an ongoing fashion, allocate them in their own space
1106
+ * Every loaded resource is served in the id space of the provider that loaded it: its id is
1107
+ * the provider's spaceId, '-', and the id it had in its source (see
1108
+ * tx/library/resource-ids.js). The spaceId is the prefix the library YAML gives for the
1109
+ * provider's source, or, if it gives none, a number - the next one, counting providers of
1110
+ * the same kind, that the YAML doesn't use as a prefix. Numbers depend on the order of the
1111
+ * sources, so a source whose ids should stay the same when the YAML changes needs a prefix.
1112
+ *
1113
+ * (Code system factories don't have ids - they are not available directly.)
1037
1114
  */
1038
1115
  assignIds() {
1039
- let ids = new Set();
1040
- // these don't have ids - not available directly for (const cs of this.codeSystemFactories) { .. }
1041
- let i = 0;
1042
- for (const cp of this.codeSystemProviders) {
1043
- cp.spaceId = String(++i);
1044
- cp.assignIds(ids);
1045
- }
1046
- i = 0;
1047
- for (const vp of this.valueSetProviders) {
1048
- vp.spaceId = String(++i);
1049
- vp.assignIds(ids);
1050
- }
1051
- i = 0;
1052
- for (const cmp of this.conceptMapProviders) {
1053
- cmp.spaceId = String(++i);
1054
- cmp.assignIds(ids);
1116
+ for (const providers of [this.codeSystemProviders, this.valueSetProviders, this.conceptMapProviders]) {
1117
+ let n = 0;
1118
+ for (const provider of providers) {
1119
+ let spaceId = this.#sourcePrefixes.get(provider);
1120
+ if (!spaceId) {
1121
+ do {
1122
+ n++;
1123
+ } while (this.#givenPrefixes.has(String(n)));
1124
+ spaceId = String(n);
1125
+ }
1126
+ provider.assignIds(spaceId);
1127
+ }
1055
1128
  }
1056
-
1057
1129
  }
1058
1130
 
1059
1131
  async close() {
package/tx/ocl/cm-ocl.cjs CHANGED
@@ -3,6 +3,7 @@ const { ConceptMap } = require('../library/conceptmap');
3
3
  const { PAGE_SIZE } = require('./shared/constants');
4
4
  const { createOclHttpClient } = require('./http/client');
5
5
  const { fetchAllPages, extractItemsAndNext } = require('./http/pagination');
6
+ const { stripIdPrefix } = require('../library/resource-ids');
6
7
 
7
8
  const DEFAULT_MAX_SEARCH_PAGES = 10;
8
9
 
@@ -24,7 +25,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
24
25
  this._canonicalBySourceUrl = new Map();
25
26
  }
26
27
 
27
- assignIds(ids) {
28
+ prefixIds() {
28
29
  if (!this.spaceId) {
29
30
  return;
30
31
  }
@@ -33,13 +34,18 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
33
34
  this._idMap.clear();
34
35
 
35
36
  for (const cm of unique) {
36
- if (!cm.id.startsWith(`${this.spaceId}-`)) {
37
- const nextId = `${this.spaceId}-${cm.id}`;
38
- cm.id = nextId;
39
- cm.jsonObj.id = nextId;
40
- }
37
+ this.#prefixId(cm);
41
38
  this._idMap.set(cm.id, cm);
42
- ids.add(`ConceptMap/${cm.id}`);
39
+ }
40
+ }
41
+
42
+ // concept maps are served in this provider's id space - including the ones that arrive
43
+ // after the spaceId is assigned
44
+ #prefixId(cm) {
45
+ if (this.spaceId && cm.id && !cm.id.startsWith(`${this.spaceId}-`)) {
46
+ const nextId = `${this.spaceId}-${cm.id}`;
47
+ cm.id = nextId;
48
+ cm.jsonObj.id = nextId;
43
49
  }
44
50
  }
45
51
 
@@ -56,7 +62,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
56
62
 
57
63
  const mappingId = this.#extractMappingId(url);
58
64
  if (mappingId) {
59
- return await this.fetchConceptMapById(mappingId);
65
+ return await this.#fetchMappingById(mappingId);
60
66
  }
61
67
 
62
68
  try {
@@ -81,14 +87,15 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
81
87
  if (this._idMap.has(id)) {
82
88
  return this._idMap.get(id);
83
89
  }
90
+ // only ids in this provider's space - the rest is the OCL mapping id
91
+ const rawId = stripIdPrefix(id, this.spaceId);
92
+ return rawId ? await this.#fetchMappingById(rawId) : null;
93
+ }
84
94
 
85
- let rawId = id;
86
- if (this.spaceId && id.startsWith(`${this.spaceId}-`)) {
87
- rawId = id.substring(this.spaceId.length + 1);
88
- }
89
-
90
- if (this._idMap.has(rawId)) {
91
- return this._idMap.get(rawId);
95
+ async #fetchMappingById(rawId) {
96
+ const id = this.spaceId ? `${this.spaceId}-${rawId}` : rawId;
97
+ if (this._idMap.has(id)) {
98
+ return this._idMap.get(id);
92
99
  }
93
100
 
94
101
  try {
@@ -378,6 +385,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
378
385
  }
379
386
 
380
387
  #indexConceptMap(cm) {
388
+ this.#prefixId(cm);
381
389
  this.conceptMapMap.set(cm.url, cm);
382
390
  if (cm.version) {
383
391
  this.conceptMapMap.set(`${cm.url}|${cm.version}`, cm);
package/tx/ocl/cs-ocl.cjs CHANGED
@@ -92,15 +92,12 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
92
92
  }
93
93
  }
94
94
 
95
- assignIds(ids) {
95
+ prefixIds() {
96
+ const codeSystems = [...new Set(this._idToCodeSystem.values())];
96
97
  this._usedIds.clear();
97
- for (const cs of this._idToCodeSystem.values()) {
98
- if (!cs.id || ids.has(`CodeSystem/${cs.id}`)) {
99
- cs.id = String(ids.size);
100
- cs.jsonObj.id = cs.id;
101
- }
102
- ids.add(`CodeSystem/${cs.id}`);
103
- this._usedIds.add(cs.id);
98
+ this._idToCodeSystem.clear();
99
+ for (const cs of codeSystems) {
100
+ this.#trackCodeSystemId(cs);
104
101
  this._idToCodeSystem.set(cs.id, cs);
105
102
  }
106
103
  }
@@ -358,9 +355,11 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
358
355
  return;
359
356
  }
360
357
 
361
- if (!cs.id || this._usedIds.has(cs.id)) {
358
+ // code systems are served in this provider's id space
359
+ const prefix = this.spaceId ? `${this.spaceId}-` : '';
360
+ if (!cs.id || !cs.id.startsWith(prefix) || this._usedIds.has(cs.id)) {
362
361
  const raw = cs.id || cs.name || cs.url || 'ocl-cs';
363
- const base = this.spaceId ? `${this.spaceId}-${raw}` : String(raw);
362
+ const base = raw.startsWith(prefix) ? String(raw) : `${prefix}${raw}`;
364
363
  let candidate = base;
365
364
  let index = 1;
366
365
  while (this._usedIds.has(candidate)) {