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.
- package/CHANGELOG.md +377 -2
- package/README.md +1 -1
- package/config-template.json +31 -1
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +208 -30
- package/publisher/validation.js +210 -0
- package/server.js +24 -15
- package/stats-db.js +127 -0
- package/stats.js +172 -6
- package/translations/Messages.properties +5 -1
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-api.md +1 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-icd11.js +805 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +141 -58
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/conceptmap.js +39 -0
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/parameter-check.js +139 -0
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/operation-context.js +220 -10
- package/tx/params.js +226 -175
- package/tx/sct/expressions.js +677 -15
- package/tx/tx.fhir.org.yml +3 -2
- package/tx/tx.js +72 -92
- package/tx/workers/batch-validate.js +11 -18
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +87 -17
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +108 -60
- package/tx/workers/lookup.js +35 -40
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +472 -47
- package/tx/workers/validate.js +83 -78
- package/tx/workers/worker.js +92 -11
- package/tx/xml/xml-base.js +264 -77
- package/tx/xversion/xv-conceptmap.js +81 -0
- package/tx/xversion/xv-parameters.js +8 -0
- package/tx/xversion/xv-valueset.js +39 -1
- package/utilities/merge-audit.py +178 -0
- package/utilities/why-merge-audit.md +39 -0
- package/tx/data/snomed-subset.txt +0 -84
package/tx/workers/batch.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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 -
|
|
349
|
-
* @param {string} 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
|
-
//
|
|
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 '
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
package/tx/workers/closure.js
CHANGED
|
@@ -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
|
|
package/tx/workers/compare.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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) {
|
package/tx/workers/expand.js
CHANGED
|
@@ -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(
|
|
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 === '
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
expansion.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2201
|
-
|
|
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
|
}
|