fhirsmith 0.14.1 → 0.15.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +3 -2
  3. package/config-template.json +9 -0
  4. package/library/changelog.js +57 -0
  5. package/library/fhir-openapi-schema.js +441 -0
  6. package/library/html-server.js +9 -3
  7. package/library/npm-audit.js +265 -0
  8. package/library/openapi-doc.js +557 -0
  9. package/library/utilities.js +21 -0
  10. package/package.json +10 -4
  11. package/packages/openapi.js +13 -0
  12. package/packages/openapi.yaml +603 -0
  13. package/packages/packages-template.html +4 -2
  14. package/packages/packages.js +286 -81
  15. package/packages/readme.md +120 -0
  16. package/registry/api.js +117 -41
  17. package/registry/crawler.js +41 -0
  18. package/registry/fhirsmith-releases.js +167 -0
  19. package/registry/model.js +13 -0
  20. package/registry/openapi.js +13 -0
  21. package/registry/openapi.yaml +311 -0
  22. package/registry/readme.md +107 -208
  23. package/registry/registry-template.html +4 -1
  24. package/registry/registry.js +198 -19
  25. package/server.js +19 -5
  26. package/testing/openapi-schemas.config.js +58 -0
  27. package/testing/openapi-schemas.json +1920 -0
  28. package/testing/openapi.js +15 -0
  29. package/testing/openapi.yaml +273 -0
  30. package/testing/readme.md +43 -1
  31. package/testing/search.js +1 -1
  32. package/testing/testing-template.html +2 -0
  33. package/testing/testing.js +24 -0
  34. package/translations/Messages.properties +1 -0
  35. package/tx/README.md +78 -4
  36. package/tx/cm/cm-api.js +25 -10
  37. package/tx/cm/cm-database.js +10 -5
  38. package/tx/cm/cm-package.js +61 -15
  39. package/tx/cs/cs-provider-api.js +22 -7
  40. package/tx/cs/cs-provider-list.js +10 -9
  41. package/tx/cs/cs-rxnorm.js +23 -6
  42. package/tx/html/tx-template.html +2 -0
  43. package/tx/library/canonical-resource.js +50 -1
  44. package/tx/library/codesystem.js +1 -0
  45. package/tx/library/conceptmap.js +1 -0
  46. package/tx/library/resource-ids.js +107 -0
  47. package/tx/library/valueset.js +16 -0
  48. package/tx/library.js +113 -26
  49. package/tx/ocl/cm-ocl.cjs +23 -15
  50. package/tx/ocl/cs-ocl.cjs +9 -10
  51. package/tx/ocl/vs-ocl.cjs +17 -17
  52. package/tx/openapi-operations.js +363 -0
  53. package/tx/openapi-schemas.config.js +118 -0
  54. package/tx/openapi-schemas.json +7903 -0
  55. package/tx/openapi.js +289 -0
  56. package/tx/openapi.yaml +151 -0
  57. package/tx/operation-context.js +15 -0
  58. package/tx/params.js +3 -12
  59. package/tx/provider.js +90 -14
  60. package/tx/tx-html.js +6 -0
  61. package/tx/tx.fhir.org.yml +16 -8
  62. package/tx/tx.js +62 -1
  63. package/tx/vs/vs-api.js +25 -10
  64. package/tx/vs/vs-database.js +14 -9
  65. package/tx/vs/vs-package.js +58 -31
  66. package/tx/vs/vs-vsac.js +20 -5
  67. package/tx/workers/expand.js +35 -16
  68. package/tx/workers/metadata.js +2 -14
  69. package/tx/workers/read.js +3 -18
  70. package/tx/workers/validate.js +65 -19
  71. package/tx/workers/worker.js +29 -8
  72. package/utilities/extract-schema-json.js +167 -0
  73. package/utilities/generate-openapi-schemas.js +49 -0
  74. package/xig/xig.js +2 -4
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':
@@ -664,7 +736,15 @@ class Library {
664
736
  const resources = await contentLoader.getResourcesByType("CodeSystem");
665
737
  let csc = 0;
666
738
  for (const resource of resources) {
667
- const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
739
+ let cs;
740
+ try {
741
+ cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
742
+ } catch (e) {
743
+ // one unacceptable resource (e.g. contained resources the server doesn't support)
744
+ // must not stop the rest of the package loading
745
+ this.log.warn(`Package ${contentLoader.pid()}: skipping CodeSystem ${resource.id || resource.filename || ''}: ${e.message}`);
746
+ continue;
747
+ }
668
748
  if (this.#isIgnored(cs.url, cs.version)) {
669
749
  this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
670
750
  continue;
@@ -775,7 +855,14 @@ class Library {
775
855
  const csEntries = await contentLoader.getResourcesByType("CodeSystem");
776
856
  let csc = 0;
777
857
  for (const entry of csEntries) {
778
- const cs = new CodeSystem(await contentLoader.loadFile(entry, contentLoader.fhirVersion()));
858
+ let cs;
859
+ try {
860
+ cs = new CodeSystem(await contentLoader.loadFile(entry, contentLoader.fhirVersion()));
861
+ } catch (e) {
862
+ // one unacceptable resource must not stop the rest of the package loading
863
+ this.log.warn(`Package ${contentLoader.pid()}: skipping CodeSystem ${entry.id || entry.filename || ''}: ${e.message}`);
864
+ continue;
865
+ }
779
866
  if (this.#isIgnored(cs.url, cs.version)) {
780
867
  this.log.info(`Ignoring CodeSystem ${cs.url}${cs.version ? '#' + cs.version : ''} (excluded by config)`);
781
868
  continue;
@@ -985,6 +1072,7 @@ class Library {
985
1072
  // Now add the existing value set providers after the FHIR core packages
986
1073
  provider.valueSetProviders.push(...this.valueSetProviders);
987
1074
  provider.conceptMapProviders.push(...this.conceptMapProviders);
1075
+ provider.applyTHOPrecedence();
988
1076
 
989
1077
  // bind UCUM common value set
990
1078
  let ucum = provider.codeSystemFactories.get("http://unitsofmeasure.org");
@@ -1015,30 +1103,29 @@ class Library {
1015
1103
  }
1016
1104
 
1017
1105
  /**
1018
- * all the loaded resources must have unique IDs for the get operation
1019
- * they must be assigned by the library on loading. providers can either assign
1020
- * ids from the global space at start up, or, if they can provide new resources
1021
- * 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.)
1022
1114
  */
1023
1115
  assignIds() {
1024
- let ids = new Set();
1025
- // these don't have ids - not available directly for (const cs of this.codeSystemFactories) { .. }
1026
- let i = 0;
1027
- for (const cp of this.codeSystemProviders) {
1028
- cp.spaceId = String(++i);
1029
- cp.assignIds(ids);
1030
- }
1031
- i = 0;
1032
- for (const vp of this.valueSetProviders) {
1033
- vp.spaceId = String(++i);
1034
- vp.assignIds(ids);
1035
- }
1036
- i = 0;
1037
- for (const cmp of this.conceptMapProviders) {
1038
- cmp.spaceId = String(++i);
1039
- 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
+ }
1040
1128
  }
1041
-
1042
1129
  }
1043
1130
 
1044
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)) {
package/tx/ocl/vs-ocl.cjs CHANGED
@@ -114,6 +114,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
114
114
  id: cached.canonicalUrl
115
115
  }, 'R5');
116
116
  this.#applyCachedCompose(valueSetObj, paramsKey);
117
+ this.#prefixId(valueSetObj);
117
118
  // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet
118
119
  this.valueSetMap.set(valueSetObj.url, valueSetObj);
119
120
  if (valueSetObj.version) {
@@ -224,7 +225,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
224
225
  }
225
226
  }
226
227
 
227
- assignIds(ids) {
228
+ prefixIds() {
228
229
  if (!this.spaceId) {
229
230
  return;
230
231
  }
@@ -233,13 +234,18 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
233
234
  this._idMap.clear();
234
235
 
235
236
  for (const vs of unique) {
236
- if (!vs.id.startsWith(`${this.spaceId}-`)) {
237
- const nextId = `${this.spaceId}-${vs.id}`;
238
- vs.id = nextId;
239
- vs.jsonObj.id = nextId;
240
- }
237
+ this.#prefixId(vs);
241
238
  this._idMap.set(vs.id, vs);
242
- ids.add(`ValueSet/${vs.id}`);
239
+ }
240
+ }
241
+
242
+ // value sets are served in this provider's id space - including the ones that arrive
243
+ // after the spaceId is assigned
244
+ #prefixId(vs) {
245
+ if (this.spaceId && vs.id && !vs.id.startsWith(`${this.spaceId}-`)) {
246
+ const nextId = `${this.spaceId}-${vs.id}`;
247
+ vs.id = nextId;
248
+ vs.jsonObj.id = nextId;
243
249
  }
244
250
  }
245
251
 
@@ -345,16 +351,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
345
351
  }
346
352
 
347
353
  #getLocalValueSetById(id) {
348
- if (this._idMap.has(id)) {
349
- return this._idMap.get(id);
350
- }
351
-
352
- if (this.spaceId && id.startsWith(`${this.spaceId}-`)) {
353
- const unprefixed = id.substring(this.spaceId.length + 1);
354
- return this._idMap.get(id) || this._idMap.get(unprefixed) || this.valueSetMap.get(unprefixed) || null;
355
- }
356
-
357
- return this._idMap.get(id) || this.valueSetMap.get(id) || null;
354
+ // _idMap is keyed by the ids the value sets are served with - prefixed, once the
355
+ // spaceId is assigned
356
+ return this._idMap.get(id) || null;
358
357
  }
359
358
 
360
359
  // eslint-disable-next-line no-unused-vars
@@ -391,6 +390,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
391
390
  }
392
391
 
393
392
  #indexValueSet(vs) {
393
+ this.#prefixId(vs);
394
394
  const existing = this.valueSetMap.get(vs.url) || null;
395
395
 
396
396
  // When fresh discovery metadata replaces a cold-cached entry, carry over