fhirsmith 0.11.2 → 0.13.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 (68) hide show
  1. package/CHANGELOG.md +377 -2
  2. package/README.md +1 -1
  3. package/config-template.json +31 -1
  4. package/library/languages.js +191 -34
  5. package/library/path-safety.js +51 -0
  6. package/library/request-token.js +56 -0
  7. package/package.json +2 -2
  8. package/packages/packages.js +17 -118
  9. package/publisher/publisher.js +208 -30
  10. package/publisher/validation.js +210 -0
  11. package/server.js +24 -15
  12. package/stats-db.js +127 -0
  13. package/stats.js +172 -6
  14. package/translations/Messages.properties +5 -1
  15. package/tx/README.md +21 -0
  16. package/tx/cs/cs-api.js +30 -1
  17. package/tx/cs/cs-api.md +1 -1
  18. package/tx/cs/cs-cs.js +1 -1
  19. package/tx/cs/cs-icd11.js +805 -0
  20. package/tx/cs/cs-lang.js +331 -85
  21. package/tx/cs/cs-loinc.js +45 -4
  22. package/tx/cs/cs-mimetypes.js +492 -21
  23. package/tx/cs/cs-snomed.js +141 -58
  24. package/tx/cs/cs-ucum.js +33 -3
  25. package/tx/cs/icd11-expressions.js +243 -0
  26. package/tx/importers/export-icd11-codesystem.js +823 -0
  27. package/tx/importers/icd11-schema.md +285 -0
  28. package/tx/importers/import-icd11.js +861 -0
  29. package/tx/importers/import-loinc.module.js +14 -7
  30. package/tx/importers/import-sct.module.js +10 -1
  31. package/tx/library/codesystem.js +21 -27
  32. package/tx/library/conceptmap.js +39 -0
  33. package/tx/library/nesting-limit.js +79 -0
  34. package/tx/library/operation-outcome.js +97 -3
  35. package/tx/library/parameter-check.js +139 -0
  36. package/tx/library/valueset.js +16 -20
  37. package/tx/library.js +23 -1
  38. package/tx/ocl/cache/cache-utils.cjs +5 -3
  39. package/tx/ocl/cs-ocl.cjs +73 -17
  40. package/tx/ocl/jobs/background-queue.cjs +6 -3
  41. package/tx/ocl/mappers/concept-mapper.cjs +9 -1
  42. package/tx/ocl/shared/patches.cjs +48 -1
  43. package/tx/ocl/vs-ocl.cjs +15 -13
  44. package/tx/operation-context.js +220 -10
  45. package/tx/params.js +226 -175
  46. package/tx/sct/expressions.js +677 -15
  47. package/tx/tx.fhir.org.yml +3 -2
  48. package/tx/tx.js +72 -92
  49. package/tx/workers/batch-validate.js +11 -18
  50. package/tx/workers/batch.js +16 -28
  51. package/tx/workers/cache-control.js +87 -17
  52. package/tx/workers/closure.js +3 -8
  53. package/tx/workers/compare.js +6 -34
  54. package/tx/workers/expand.js +108 -60
  55. package/tx/workers/lookup.js +35 -40
  56. package/tx/workers/read.js +6 -40
  57. package/tx/workers/search.js +2 -8
  58. package/tx/workers/subsumes.js +58 -41
  59. package/tx/workers/translate.js +472 -47
  60. package/tx/workers/validate.js +83 -78
  61. package/tx/workers/worker.js +92 -11
  62. package/tx/xml/xml-base.js +264 -77
  63. package/tx/xversion/xv-conceptmap.js +81 -0
  64. package/tx/xversion/xv-parameters.js +8 -0
  65. package/tx/xversion/xv-valueset.js +39 -1
  66. package/utilities/merge-audit.py +178 -0
  67. package/utilities/why-merge-audit.md +39 -0
  68. package/tx/data/snomed-subset.txt +0 -84
@@ -5,7 +5,7 @@
5
5
  //
6
6
 
7
7
  const { TerminologyWorker } = require('./worker');
8
- const { Issue, OperationOutcome } = require('../library/operation-outcome');
8
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
9
9
  const {debugLog} = require("../operation-context");
10
10
 
11
11
  class BatchWorker extends TerminologyWorker {
@@ -47,8 +47,7 @@ class BatchWorker extends TerminologyWorker {
47
47
  oo.addIssue(error);
48
48
  return res.status(error.statusCode || 500).json(oo.jsonObj);
49
49
  } else {
50
- return res.status(error.statusCode || 500).json(this.operationOutcome(
51
- 'error', error.issueCode || 'exception', error.message));
50
+ return res.status(error.statusCode || 500).json(outcomeFromError(error));
52
51
  }
53
52
  }
54
53
  }
@@ -163,9 +162,8 @@ class BatchWorker extends TerminologyWorker {
163
162
  this.log.error(error);
164
163
  debugLog(error);
165
164
  const statusCode = error.statusCode || 500;
166
- const issueCode = error.issueCode || 'exception';
167
-
168
- return this.errorEntry(statusCode, issueCode, error.message);
165
+ // on a plain Error, issueCode is the FHIR issue type and txIssueType the tx-issue-type
166
+ return this.errorEntry(statusCode, error.issueCode || 'exception', error.message, error.txIssueType);
169
167
  }
170
168
  }
171
169
 
@@ -319,19 +317,13 @@ class BatchWorker extends TerminologyWorker {
319
317
  /**
320
318
  * Build an error response entry
321
319
  * @param {number} statusCode - HTTP status code
322
- * @param {string} issueCode - FHIR issue code
320
+ * @param {string} issueCode - FHIR issue type
323
321
  * @param {string} message - Error message
322
+ * @param {string} [txIssueType] - tx-issue-type; defaulted from issueCode when not given
324
323
  * @returns {Object} Error entry
325
324
  */
326
- errorEntry(statusCode, issueCode, message) {
327
- const outcome = {
328
- resourceType: 'OperationOutcome',
329
- issue: [{
330
- severity: 'error',
331
- code: issueCode,
332
- diagnostics: message
333
- }]
334
- };
325
+ errorEntry(statusCode, issueCode, message, txIssueType = null) {
326
+ const outcome = buildOperationOutcome('error', issueCode, message, txIssueType);
335
327
 
336
328
  return {
337
329
  resource: outcome,
@@ -343,21 +335,17 @@ class BatchWorker extends TerminologyWorker {
343
335
  }
344
336
 
345
337
  /**
346
- * Build an OperationOutcome
338
+ * Build an OperationOutcome. Delegates to the shared builder so that every outcome this
339
+ * server emits has details.text and a tx-issue-type coding: diagnostics is stripped by
340
+ * the test harness, so nothing a caller needs may live there.
347
341
  * @param {string} severity - error, warning, information
348
- * @param {string} code - Issue code
349
- * @param {string} message - Diagnostic message
342
+ * @param {string} code - FHIR issue type
343
+ * @param {string} message - the human readable account of the problem
344
+ * @param {string} [txIssueType] - tx-issue-type; defaulted from code when not given
350
345
  * @returns {Object} OperationOutcome resource
351
346
  */
352
- operationOutcome(severity, code, message) {
353
- return {
354
- resourceType: 'OperationOutcome',
355
- issue: [{
356
- severity,
357
- code,
358
- diagnostics: message
359
- }]
360
- };
347
+ operationOutcome(severity, code, message, txIssueType = null) {
348
+ return buildOperationOutcome(severity, code, message, txIssueType);
361
349
  }
362
350
  }
363
351
 
@@ -5,20 +5,18 @@
5
5
  // POST /$cache-control?mode=start - as above, optionally front-loading resources
6
6
  // GET /$cache-control?mode=end - tell the server it can release the cache now
7
7
  // POST /$cache-control?mode=end
8
- // (mode=check is reserved for later - report whether a cache is still valid + stats)
8
+ // GET /$cache-control?mode=check - is this cache still alive? (and keep it alive)
9
+ // POST /$cache-control?mode=check
9
10
  //
10
11
  // This is the explicit replacement for the implicit `cache-id` parameter protocol:
11
12
  // the server owns the cache-id namespace, so it can authoritatively reject an
12
13
  // unknown/expired cache later instead of failing obscurely deep inside a validation.
13
14
  //
14
- // NOTE: this is scaffolding. start()/end() currently parse the request into a
15
- // Parameters resource (the same way validate.js does) but do not yet create or
16
- // release anything. The behaviour is filled in by later steps.
17
- //
18
15
 
19
16
  const crypto = require('crypto');
20
17
  const { TerminologyWorker, CACHE_ID_HEADER } = require('./worker');
21
18
  const { Parameters } = require('../library/parameters');
19
+ const { Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
22
20
  const { debugLog } = require('../operation-context');
23
21
 
24
22
  class CacheControlWorker extends TerminologyWorker {
@@ -61,9 +59,11 @@ class CacheControlWorker extends TerminologyWorker {
61
59
  return await this.start(req, res);
62
60
  case 'end':
63
61
  return await this.end(req, res);
62
+ case 'check':
63
+ return await this.check(req, res);
64
64
  default:
65
65
  return res.status(400).json(this.operationOutcome('error', 'invalid',
66
- `$cache-control requires a 'mode' of 'start' or 'end'` +
66
+ `$cache-control requires a 'mode' of 'start', 'end' or 'check'` +
67
67
  (mode ? ` (got '${mode}')` : ` (none supplied)`)));
68
68
  }
69
69
  } catch (error) {
@@ -71,8 +71,8 @@ class CacheControlWorker extends TerminologyWorker {
71
71
  debugLog(error);
72
72
  req.logInfo = this.usedSources.join('|') + ' - error' + (error.msgId ? ' ' + error.msgId : '');
73
73
  const statusCode = error.statusCode || 500;
74
- const issueCode = error.issueCode || 'exception';
75
- return res.status(statusCode).json(this.operationOutcome('error', issueCode, error.message));
74
+ return res.status(statusCode).json(
75
+ outcomeFromError(error));
76
76
  }
77
77
  }
78
78
 
@@ -193,6 +193,82 @@ class CacheControlWorker extends TerminologyWorker {
193
193
  return res.status(200).json({ resourceType: 'Parameters', parameter: [] });
194
194
  }
195
195
 
196
+ /**
197
+ * mode=check: report whether the cache named by the `${CACHE_ID_HEADER}` header is
198
+ * still alive, and - if it is - reset its idle clock.
199
+ *
200
+ * This is a keepalive as much as a probe, and deliberately so. A client's own
201
+ * local cache absorbs most of its terminology work, so the server can see nothing
202
+ * from a client that is still very much running and still relying on its cache;
203
+ * the cache then times out mid-job and the next request that finally does reach
204
+ * the server fails. Checking is exactly what a client that still cares about its
205
+ * cache does, so a check counts as use. (The alternative - a read-only `check`
206
+ * plus a separate `touch` - is two operations for one job, and there is no useful
207
+ * case for asking "is my cache alive?" while wanting it to expire anyway.)
208
+ *
209
+ * An unknown cache-id here is a 200 with `valid` = false, NOT the 404 that the
210
+ * validation path returns. A probe must be able to tell "the server is up and
211
+ * says my cache is gone" from "I could not reach the server" - those call for
212
+ * opposite responses, and collapsing them into one failure loses that. This also
213
+ * matches mode=end, which likewise tolerates an id the server doesn't have. The
214
+ * `outcome` parameter carries the same coded issue the validation path would have
215
+ * raised, so a client that wants the reason (never issued here / closed by the
216
+ * client / expired after N minutes idle) has it without a second call.
217
+ *
218
+ * @param {express.Request} req
219
+ * @param {express.Response} res
220
+ */
221
+ async check(req, res) {
222
+ const cache = this.opContext.resourceCache;
223
+ if (!cache) {
224
+ return res.status(500).json(this.operationOutcome('error', 'exception',
225
+ 'No resource cache is available on this endpoint'));
226
+ }
227
+
228
+ const cacheId = req.headers[CACHE_ID_HEADER];
229
+ if (!cacheId) {
230
+ return res.status(400).json(this.operationOutcome('error', 'invalid',
231
+ `$cache-control mode=check requires the cache-id in the '${CACHE_ID_HEADER}' header`));
232
+ }
233
+
234
+ // Read the status BEFORE touching: the idle time is the interesting part of the
235
+ // answer, and refreshing first would report zero every time.
236
+ const status = cache.status(cacheId);
237
+
238
+ if (!status.exists) {
239
+ const { messageId, params } = cache.describeMissing(cacheId);
240
+ const issue = new Issue('error', 'not-found', null, messageId,
241
+ this.i18n.translate(messageId, this.opContext.langs, params),
242
+ 'cache-id-unknown', 404).asIssue();
243
+ this.log.info(`cache-id '${cacheId}': check -> not valid (${messageId})`);
244
+ return res.status(200).json({
245
+ resourceType: 'Parameters',
246
+ parameter: [
247
+ { name: 'cache-id', valueId: cacheId },
248
+ { name: 'valid', valueBoolean: false },
249
+ { name: 'outcome', resource: { resourceType: 'OperationOutcome', issue: [issue] } }
250
+ ]
251
+ });
252
+ }
253
+
254
+ cache.touch(cacheId);
255
+
256
+ const parameter = [
257
+ { name: 'cache-id', valueId: cacheId },
258
+ { name: 'valid', valueBoolean: true },
259
+ { name: 'sealed', valueBoolean: status.sealed },
260
+ { name: 'resource-count', valueUnsignedInt: status.resources },
261
+ { name: 'idle', valueUnsignedInt: Math.floor(status.idleMs / 1000) }
262
+ ];
263
+ // The timeout lets a client work out how often it needs to check, instead of
264
+ // guessing against a number it cannot see. Omitted if this server isn't saying.
265
+ if (status.timeoutMs !== null && status.timeoutMs !== undefined) {
266
+ parameter.push({ name: 'timeout', valueUnsignedInt: Math.floor(status.timeoutMs / 1000) });
267
+ }
268
+
269
+ return res.status(200).json({ resourceType: 'Parameters', parameter });
270
+ }
271
+
196
272
  /**
197
273
  * Build an OperationOutcome.
198
274
  * @param {string} severity - error, warning, information
@@ -200,15 +276,9 @@ class CacheControlWorker extends TerminologyWorker {
200
276
  * @param {string} message - Diagnostic message
201
277
  * @returns {Object} OperationOutcome resource
202
278
  */
203
- operationOutcome(severity, code, message) {
204
- return {
205
- resourceType: 'OperationOutcome',
206
- issue: [{
207
- severity,
208
- code,
209
- details: { text: message }
210
- }]
211
- };
279
+ operationOutcome(severity, code, message, txIssueType = null) {
280
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
281
+ return buildOperationOutcome(severity, code, message, txIssueType);
212
282
  }
213
283
  }
214
284
 
@@ -5,6 +5,8 @@
5
5
  // POST /ConceptMap/$closure
6
6
  //
7
7
 
8
+ const { buildOperationOutcome } = require('../library/operation-outcome');
9
+
8
10
  class ClosureWorker {
9
11
  /**
10
12
  * Handle a $closure request
@@ -18,14 +20,7 @@ class ClosureWorker {
18
20
  this.log.debug('ConceptMap $closure with params:', params);
19
21
 
20
22
  // TODO: Implement closure logic using provider
21
- res.status(501).json({
22
- resourceType: 'OperationOutcome',
23
- issue: [{
24
- severity: 'error',
25
- code: 'not-supported',
26
- diagnostics: 'ConceptMap $closure operation not yet implemented'
27
- }]
28
- });
23
+ res.status(501).json(buildOperationOutcome('error', 'not-supported', 'ConceptMap $closure operation not yet implemented'));
29
24
  }
30
25
  }
31
26
 
@@ -11,7 +11,7 @@
11
11
  const { TerminologyWorker } = require('./worker');
12
12
  const {TxParameters} = require("../params");
13
13
  const {Extensions} = require("../library/extensions");
14
- const {Issue, OperationOutcome} = require("../library/operation-outcome");
14
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
15
15
  const ValueSet = require("../library/valueset");
16
16
  const {ValueSetExpander} = require("./expand");
17
17
  const {SearchFilterText} = require("../library/designations");
@@ -59,18 +59,7 @@ class CompareWorker extends TerminologyWorker {
59
59
  oo.addIssue(error);
60
60
  return res.status(error.statusCode || 500).json(oo.jsonObj);
61
61
  } else {
62
- const issueCode = error.issueCode || 'exception';
63
- return res.status(statusCode).json({
64
- resourceType: 'OperationOutcome',
65
- issue: [{
66
- severity: 'error',
67
- code: issueCode,
68
- details: {
69
- text: error.message
70
- },
71
- diagnostics: error.message
72
- }]
73
- });
62
+ return res.status(statusCode).json(outcomeFromError(error));
74
63
  }
75
64
  }
76
65
  }
@@ -89,18 +78,7 @@ class CompareWorker extends TerminologyWorker {
89
78
  debugLog(error);
90
79
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
91
80
  const statusCode = error.statusCode || 500;
92
- const issueCode = error.issueCode || 'exception';
93
- return res.status(statusCode).json({
94
- resourceType: 'OperationOutcome',
95
- issue: [{
96
- severity: 'error',
97
- code: issueCode,
98
- details: {
99
- text : error.message
100
- },
101
- diagnostics: error.message
102
- }]
103
- });
81
+ return res.status(statusCode).json(outcomeFromError(error));
104
82
  }
105
83
  }
106
84
 
@@ -158,15 +136,9 @@ class CompareWorker extends TerminologyWorker {
158
136
  * @param {string} message - Diagnostic message
159
137
  * @returns {Object} OperationOutcome resource
160
138
  */
161
- operationOutcome(severity, code, message) {
162
- return {
163
- resourceType: 'OperationOutcome',
164
- issue: [{
165
- severity,
166
- code,
167
- diagnostics: message
168
- }]
169
- };
139
+ operationOutcome(severity, code, message, txIssueType = null) {
140
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
141
+ return buildOperationOutcome(severity, code, message, txIssueType);
170
142
  }
171
143
 
172
144
  async readValueSet(res, prefix, params) {
@@ -14,7 +14,7 @@ const {Designations, SearchFilterText} = require("../library/designations");
14
14
  const {Extensions} = require("../library/extensions");
15
15
  const {getValuePrimitive, getValueName, validateParameter} = require("../../library/utilities");
16
16
  const {div} = require("../../library/html");
17
- const {Issue, OperationOutcome} = require("../library/operation-outcome");
17
+ const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
18
18
  const crypto = require('crypto');
19
19
  const ValueSet = require("../library/valueset");
20
20
  const {VersionUtilities} = require("../../library/version-utilities");
@@ -447,8 +447,21 @@ class ValueSetExpander {
447
447
  }
448
448
  }
449
449
 
450
+ // '*' asks for every property the code system defines for the concept (R6's
451
+ // ValueSet.compose.property allows it, and a request can name it too). It is handled
452
+ // here rather than by expanding it into a list of names, because which properties
453
+ // exist is a property of the concept, not of the request
454
+ if (this.params.properties.includes('*') && csProps != null && cs != null) {
455
+ for (const cp of csProps) {
456
+ const vn = getValueName(cp);
457
+ this.defineProperty(expansion, n, this.getPropUrl(cs, cp.code, cp), cp.code, vn, cp[vn], true);
458
+ }
459
+ }
460
+
450
461
  for (const pn of this.params.properties) {
451
- if (pn === 'definition') {
462
+ if (pn === '*') {
463
+ continue; // done above
464
+ } else if (pn === 'definition') {
452
465
  if (definition) {
453
466
  this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#definition', pn, "valueString", definition);
454
467
  }
@@ -460,7 +473,9 @@ class ValueSetExpander {
460
473
  if (cp.code === pn) {
461
474
  let vn = getValueName(cp);
462
475
  let v = cp[vn];
463
- this.defineProperty(expansion, n, this.getPropUrl(cs, pn, cp), pn, vn, v);
476
+ // a concept may carry the same property more than once (the
477
+ // "dup" property in the test code system has two values)
478
+ this.defineProperty(expansion, n, this.getPropUrl(cs, pn, cp), pn, vn, v, true);
464
479
  }
465
480
  }
466
481
  }
@@ -571,19 +586,24 @@ class ValueSetExpander {
571
586
  }
572
587
  this.checkResourceCanonicalStatus(expansion, vs, this.valueSet);
573
588
 
589
+ const imported = [];
574
590
  for (const c of vs.expansion.contains || []) {
575
591
  await this.worker.checkAndYield('importValueSet');
576
- count += await this.importValueSetItem(null, c, imports, offset);
592
+ count += await this.importValueSetItem(null, c, imports, offset, imported);
577
593
  }
594
+ this.importPropertyDefinitions(expansion, vs.expansion.property, imported);
578
595
  return count;
579
596
  }
580
597
 
581
- async importValueSetItem(p, c, imports, offset) {
598
+ async importValueSetItem(p, c, imports, offset, imported) {
582
599
  let count = 0;
583
600
  await this.worker.checkAndYield('importValueSetItem');
584
601
  const s = this.keyC(c);
585
602
  if (this.passesImports(imports, c.system, c.code, offset) && !this.map.has(s) && !this.isExcluded(c.system, c.version, c.code)) {
586
603
  count++;
604
+ if (imported) {
605
+ imported.push(c);
606
+ }
587
607
  this.fullList.push(c);
588
608
  if (p != null) {
589
609
  if (!p.contains) {p.contains = [] }
@@ -595,7 +615,7 @@ class ValueSetExpander {
595
615
  }
596
616
  for (const cc of c.contains || []) {
597
617
  await this.worker.checkAndYield('importValueSetItem');
598
- count += await this.importValueSetItem(c, cc, imports, offset);
618
+ count += await this.importValueSetItem(c, cc, imports, offset, imported);
599
619
  }
600
620
  return count;
601
621
  }
@@ -1087,7 +1107,7 @@ class ValueSetExpander {
1087
1107
  this.worker.opContext.log('prep filters');
1088
1108
  const prep = await cs.getPrepContext(true);
1089
1109
  if (!filter.isNull) {
1090
- await cs.searchFilter(filter, prep, true);
1110
+ await cs.searchFilter(prep, filter, true);
1091
1111
  }
1092
1112
 
1093
1113
  if (cs.specialEnumeration()) {
@@ -1679,35 +1699,91 @@ class ValueSetExpander {
1679
1699
  return this.keyS(contains.system, contains.version, contains.code);
1680
1700
  }
1681
1701
 
1682
- defineProperty(expansion, contains, url, code, valueName, value) {
1683
- if (value === undefined || value == null) {
1702
+ /**
1703
+ * Record a property definition in expansion.property, and return the code to
1704
+ * use for the per-concept value (an existing definition's code wins).
1705
+ *
1706
+ * @param {Object} expansion
1707
+ * @param {string} url
1708
+ * @param {string} code
1709
+ * @returns {string}
1710
+ */
1711
+ declareProperty(expansion, url, code) {
1712
+ if (!expansion.property) {
1713
+ expansion.property = [];
1714
+ }
1715
+ // match on the uri only when there is one. A code system may declare several properties with
1716
+ // no uri at all, and those are told apart by their code - matching on an undefined uri would
1717
+ // collapse all of them into whichever one was declared first
1718
+ let pd = url
1719
+ ? expansion.property.find(t1 => t1.uri == url || t1.code == code)
1720
+ : expansion.property.find(t1 => t1.code == code);
1721
+ if (!pd) {
1722
+ pd = {};
1723
+ expansion.property.push(pd);
1724
+ pd.code = code;
1725
+ if (url) {
1726
+ pd.uri = url;
1727
+ }
1728
+ } else if (url && !pd.uri) {
1729
+ pd.uri = url;
1730
+ }
1731
+ if (url && pd.uri != url) {
1732
+ throw new Error('URL mismatch on expansion: ' + pd.uri + ' vs ' + url + ' for code ' + code);
1733
+ }
1734
+ return pd.code;
1735
+ }
1736
+
1737
+ /**
1738
+ * Carry the property definitions of an imported expansion across to this one.
1739
+ *
1740
+ * Imported concepts are copied whole (see importValueSetItem), properties and
1741
+ * all, but the definitions that give those properties a uri live on the
1742
+ * imported expansion, so without this the outer expansion has concepts
1743
+ * carrying a property that it never declares. Only definitions for properties
1744
+ * that survived the import are copied.
1745
+ *
1746
+ * @param {Object} expansion - the expansion being built
1747
+ * @param {Array} definitions - the imported expansion's `property`
1748
+ * @param {Array} items - the concepts actually taken from the import
1749
+ */
1750
+ importPropertyDefinitions(expansion, definitions, items) {
1751
+ if (!expansion || !definitions || definitions.length === 0 || items.length === 0) {
1684
1752
  return;
1685
1753
  }
1686
- // we only define it if the code system has a definition
1687
- if (url) {
1688
- if (!expansion.property) {
1689
- expansion.property = [];
1754
+ const used = new Set();
1755
+ for (const item of items) {
1756
+ for (const property of item.property || []) {
1757
+ used.add(property.code);
1690
1758
  }
1691
- let pd = expansion.property.find(t1 => t1.uri == url || t1.code == code);
1692
- if (!pd) {
1693
- pd = {};
1694
- expansion.property.push(pd);
1695
- pd.uri = url;
1696
- pd.code = code;
1697
- } else if (!pd.uri) {
1698
- pd.uri = url
1699
- }
1700
- if (pd.uri != url) {
1701
- throw new Error('URL mismatch on expansion: ' + pd.uri + ' vs ' + url + ' for code ' + code);
1702
- } else {
1703
- code = pd.code;
1759
+ }
1760
+ for (const definition of definitions) {
1761
+ if (used.has(definition.code)) {
1762
+ this.declareProperty(expansion, definition.uri, definition.code);
1704
1763
  }
1705
1764
  }
1765
+ }
1766
+
1767
+ /**
1768
+ * @param {boolean} [repeatable] - true when the code system may carry more than
1769
+ * one value for this property on one concept (only the code system property
1770
+ * loop passes this). Single-valued properties keep last-one-wins, so a status
1771
+ * derived from two sources still yields one entry.
1772
+ */
1773
+ defineProperty(expansion, contains, url, code, valueName, value, repeatable) {
1774
+ if (value === undefined || value == null) {
1775
+ return;
1776
+ }
1777
+ // every property that appears in contains.property has to be declared in expansion.property,
1778
+ // whether or not the code system gave it a uri
1779
+ code = this.declareProperty(expansion, url, code);
1706
1780
 
1707
1781
  if (!contains.property) {
1708
1782
  contains.property = [];
1709
1783
  }
1710
- let pdv = contains.property.find(t2 => t2.code == code);
1784
+ let pdv = repeatable
1785
+ ? contains.property.find(t2 => t2.code == code && t2[valueName] === value)
1786
+ : contains.property.find(t2 => t2.code == code);
1711
1787
  if (!pdv) {
1712
1788
  pdv = {};
1713
1789
  contains.property.push(pdv);
@@ -1893,18 +1969,7 @@ class ExpandWorker extends TerminologyWorker {
1893
1969
  oo.addIssue(error);
1894
1970
  return res.status(error.statusCode || 500).json(oo.jsonObj);
1895
1971
  } else {
1896
- const issueCode = error.issueCode || 'exception';
1897
- return res.status(statusCode).json({
1898
- resourceType: 'OperationOutcome',
1899
- issue: [{
1900
- severity: 'error',
1901
- code: issueCode,
1902
- details: {
1903
- text: error.message
1904
- },
1905
- diagnostics: error.message
1906
- }]
1907
- });
1972
+ return res.status(statusCode).json(outcomeFromError(error));
1908
1973
  }
1909
1974
  }
1910
1975
  }
@@ -1923,18 +1988,7 @@ class ExpandWorker extends TerminologyWorker {
1923
1988
  debugLog(error);
1924
1989
  req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
1925
1990
  const statusCode = error.statusCode || 500;
1926
- const issueCode = error.issueCode || 'exception';
1927
- return res.status(statusCode).json({
1928
- resourceType: 'OperationOutcome',
1929
- issue: [{
1930
- severity: 'error',
1931
- code: issueCode,
1932
- details: {
1933
- text : error.message
1934
- },
1935
- diagnostics: error.message
1936
- }]
1937
- });
1991
+ return res.status(statusCode).json(outcomeFromError(error));
1938
1992
  }
1939
1993
  }
1940
1994
 
@@ -2196,15 +2250,9 @@ class ExpandWorker extends TerminologyWorker {
2196
2250
  * @param {string} message - Diagnostic message
2197
2251
  * @returns {Object} OperationOutcome resource
2198
2252
  */
2199
- operationOutcome(severity, code, message) {
2200
- return {
2201
- resourceType: 'OperationOutcome',
2202
- issue: [{
2203
- severity,
2204
- code,
2205
- diagnostics: message
2206
- }]
2207
- };
2253
+ operationOutcome(severity, code, message, txIssueType = null) {
2254
+ // the shared builder, so that every outcome has details.text and a tx-issue-type coding
2255
+ return buildOperationOutcome(severity, code, message, txIssueType);
2208
2256
  }
2209
2257
 
2210
2258
  }