fhirsmith 0.4.2 → 0.5.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 +12 -0
- package/README.md +1 -1
- package/library/cron-utilities.js +136 -0
- package/library/html-server.js +13 -29
- package/library/html.js +3 -8
- package/library/languages.js +160 -37
- package/library/package-manager.js +48 -1
- package/library/utilities.js +100 -19
- package/package.json +2 -2
- package/packages/package-crawler.js +6 -1
- package/packages/packages.js +38 -54
- package/publisher/publisher.js +19 -27
- package/registry/api.js +11 -10
- package/registry/crawler.js +31 -29
- package/registry/model.js +5 -26
- package/registry/registry.js +32 -41
- package/server.js +53 -5
- package/shl/shl.js +0 -18
- package/static/assets/js/statuspage.js +1 -9
- package/stats.js +39 -1
- package/token/token.js +14 -9
- package/translations/Messages.properties +2 -1
- package/tx/README.md +17 -6
- package/tx/cs/cs-api.js +19 -1
- package/tx/cs/cs-base.js +77 -0
- package/tx/cs/cs-country.js +46 -0
- package/tx/cs/cs-cpt.js +9 -5
- package/tx/cs/cs-cs.js +27 -13
- package/tx/cs/cs-lang.js +60 -22
- package/tx/cs/cs-loinc.js +69 -98
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-ndc.js +6 -0
- package/tx/cs/cs-omop.js +16 -15
- package/tx/cs/cs-rxnorm.js +23 -1
- package/tx/cs/cs-snomed.js +283 -40
- package/tx/cs/cs-ucum.js +90 -70
- package/tx/importers/import-sct.module.js +371 -35
- package/tx/importers/readme.md +117 -7
- package/tx/library/bundle.js +5 -0
- package/tx/library/capabilitystatement.js +3 -142
- package/tx/library/codesystem.js +19 -173
- package/tx/library/conceptmap.js +4 -218
- package/tx/library/designations.js +14 -1
- package/tx/library/extensions.js +7 -0
- package/tx/library/namingsystem.js +3 -89
- package/tx/library/operation-outcome.js +8 -3
- package/tx/library/parameters.js +3 -2
- package/tx/library/renderer.js +10 -6
- package/tx/library/terminologycapabilities.js +3 -243
- package/tx/library/valueset.js +3 -235
- package/tx/library.js +100 -13
- package/tx/operation-context.js +23 -4
- package/tx/params.js +35 -38
- package/tx/provider.js +6 -5
- package/tx/sct/expressions.js +12 -3
- package/tx/tx-html.js +80 -89
- package/tx/tx.fhir.org.yml +6 -5
- package/tx/tx.js +163 -13
- package/tx/vs/vs-database.js +56 -39
- package/tx/vs/vs-package.js +21 -2
- package/tx/vs/vs-vsac.js +175 -39
- package/tx/workers/batch-validate.js +2 -0
- package/tx/workers/batch.js +2 -0
- package/tx/workers/expand.js +132 -112
- package/tx/workers/lookup.js +33 -14
- package/tx/workers/metadata.js +2 -2
- package/tx/workers/read.js +3 -2
- package/tx/workers/related.js +574 -0
- package/tx/workers/search.js +46 -9
- package/tx/workers/subsumes.js +13 -3
- package/tx/workers/translate.js +7 -3
- package/tx/workers/validate.js +258 -285
- package/tx/workers/worker.js +43 -39
- package/tx/xml/bundle-xml.js +237 -0
- package/tx/xml/xml-base.js +215 -64
- package/tx/xversion/xv-bundle.js +71 -0
- package/tx/xversion/xv-capabiliityStatement.js +137 -0
- package/tx/xversion/xv-codesystem.js +169 -0
- package/tx/xversion/xv-conceptmap.js +224 -0
- package/tx/xversion/xv-namingsystem.js +88 -0
- package/tx/xversion/xv-operationoutcome.js +27 -0
- package/tx/xversion/xv-parameters.js +87 -0
- package/tx/xversion/xv-resource.js +45 -0
- package/tx/xversion/xv-terminologyCapabilities.js +214 -0
- package/tx/xversion/xv-valueset.js +234 -0
- package/utilities/dev-proxy-server.js +126 -0
- package/utilities/explode-results.js +58 -0
- package/utilities/split-by-system.js +198 -0
- package/utilities/vsac-cs-fetcher.js +0 -0
- package/{windows-install.js → utilities/windows-install.js} +2 -0
- package/vcl/vcl.js +0 -18
- package/xig/xig.js +108 -99
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Related Worker - Handles ValueSet $related operation
|
|
3
|
+
//
|
|
4
|
+
// GET /ValueSet/{id}/$related
|
|
5
|
+
// GET /ValueSet/$related?url=...&version=...
|
|
6
|
+
// POST /ValueSet/$related (form body or Parameters with url)
|
|
7
|
+
// POST /ValueSet/$related (body is ValueSet resource)
|
|
8
|
+
// POST /ValueSet/$related (body is Parameters with valueSet parameter)
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
const { TerminologyWorker } = require('./worker');
|
|
12
|
+
const {TxParameters} = require("../params");
|
|
13
|
+
const {Extensions} = require("../library/extensions");
|
|
14
|
+
const {Issue, OperationOutcome} = require("../library/operation-outcome");
|
|
15
|
+
const ValueSet = require("../library/valueset");
|
|
16
|
+
const {ValueSetExpander} = require("./expand");
|
|
17
|
+
const {SearchFilterText} = require("../library/designations");
|
|
18
|
+
const {ArrayMatcher} = require("../../library/utilities");
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RelatedWorker extends TerminologyWorker {
|
|
22
|
+
/**
|
|
23
|
+
* @param {OperationContext} opContext - Operation context
|
|
24
|
+
* @param {Logger} log - Logger instance
|
|
25
|
+
* @param {Provider} provider - Provider for code systems and resources
|
|
26
|
+
* @param {LanguageDefinitions} languages - Language definitions
|
|
27
|
+
* @param {I18nSupport} i18n - Internationalization support
|
|
28
|
+
*/
|
|
29
|
+
constructor(opContext, log, provider, languages, i18n) {
|
|
30
|
+
super(opContext, log, provider, languages, i18n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get operation name
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
opName() {
|
|
38
|
+
return 'related';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle a type-level $related request
|
|
43
|
+
* GET/POST /ValueSet/$related
|
|
44
|
+
* @param {express.Request} req - Express request
|
|
45
|
+
* @param {express.Response} res - Express response
|
|
46
|
+
*/
|
|
47
|
+
async handle(req, res) {
|
|
48
|
+
try {
|
|
49
|
+
await this.handleTypeLevelRelated(req, res);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.log.error(error);
|
|
52
|
+
this.debugLog(error);
|
|
53
|
+
req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
|
|
54
|
+
const statusCode = error.statusCode || 500;
|
|
55
|
+
if (error instanceof Issue) {
|
|
56
|
+
let oo = new OperationOutcome();
|
|
57
|
+
oo.addIssue(error);
|
|
58
|
+
return res.status(error.statusCode || 500).json(oo.jsonObj);
|
|
59
|
+
} else {
|
|
60
|
+
const issueCode = error.issueCode || 'exception';
|
|
61
|
+
return res.status(statusCode).json({
|
|
62
|
+
resourceType: 'OperationOutcome',
|
|
63
|
+
issue: [{
|
|
64
|
+
severity: 'error',
|
|
65
|
+
code: issueCode,
|
|
66
|
+
details: {
|
|
67
|
+
text: error.message
|
|
68
|
+
},
|
|
69
|
+
diagnostics: error.message
|
|
70
|
+
}]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handle an instance-level $related request
|
|
78
|
+
* GET/POST /ValueSet/{id}/$related
|
|
79
|
+
* @param {express.Request} req - Express request
|
|
80
|
+
* @param {express.Response} res - Express response
|
|
81
|
+
*/
|
|
82
|
+
async handleInstance(req, res) {
|
|
83
|
+
try {
|
|
84
|
+
await this.handleInstanceLevelRelated(req, res);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.log.error(error);
|
|
87
|
+
this.debugLog(error);
|
|
88
|
+
req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : "");
|
|
89
|
+
const statusCode = error.statusCode || 500;
|
|
90
|
+
const issueCode = error.issueCode || 'exception';
|
|
91
|
+
return res.status(statusCode).json({
|
|
92
|
+
resourceType: 'OperationOutcome',
|
|
93
|
+
issue: [{
|
|
94
|
+
severity: 'error',
|
|
95
|
+
code: issueCode,
|
|
96
|
+
details: {
|
|
97
|
+
text : error.message
|
|
98
|
+
},
|
|
99
|
+
diagnostics: error.message
|
|
100
|
+
}]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Handle type-level $related: /ValueSet/$related
|
|
107
|
+
* ValueSet identified by url, or provided directly in body
|
|
108
|
+
*/
|
|
109
|
+
async handleTypeLevelRelated(req, res) {
|
|
110
|
+
this.deadCheck('related-type-level');
|
|
111
|
+
|
|
112
|
+
let params = req.body;
|
|
113
|
+
this.addHttpParams(req, params);
|
|
114
|
+
this.setupAdditionalResources(params);
|
|
115
|
+
let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false);
|
|
116
|
+
txp.readParams(params);
|
|
117
|
+
|
|
118
|
+
this.params = txp;
|
|
119
|
+
|
|
120
|
+
let thisVS = await this.readValueSet(res, "this", params, txp);
|
|
121
|
+
let otherVS = await this.readValueSet(res, "other", params, txp);
|
|
122
|
+
|
|
123
|
+
const result = await this.doRelated(txp, thisVS, otherVS);
|
|
124
|
+
return res.json(result);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle instance-level related: /ValueSet/{id}/$related
|
|
129
|
+
* ValueSet identified by resource ID
|
|
130
|
+
*/
|
|
131
|
+
async handleInstanceLevelRelated(req, res) {
|
|
132
|
+
this.deadCheck('related-instance-level');
|
|
133
|
+
|
|
134
|
+
let params = req.body;
|
|
135
|
+
this.addHttpParams(req, params);
|
|
136
|
+
this.setupAdditionalResources(params);
|
|
137
|
+
let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false);
|
|
138
|
+
txp.readParams(params);
|
|
139
|
+
|
|
140
|
+
const { id } = req.params;
|
|
141
|
+
// Find the ValueSet by ID
|
|
142
|
+
const thisVS = await this.provider.getValueSetById(this.opContext, id);
|
|
143
|
+
if (!thisVS) {
|
|
144
|
+
return res.status(404).json(this.operationOutcome('error', 'not-found',
|
|
145
|
+
`ValueSet/${id} not found`));
|
|
146
|
+
}
|
|
147
|
+
let otherVS = await this.readValueSet(res, "other", params, txp);
|
|
148
|
+
|
|
149
|
+
const result = await this.doRelated(txp, thisVS, otherVS);
|
|
150
|
+
return res.json(result);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build an OperationOutcome
|
|
155
|
+
* @param {string} severity - error, warning, information
|
|
156
|
+
* @param {string} code - Issue code
|
|
157
|
+
* @param {string} message - Diagnostic message
|
|
158
|
+
* @returns {Object} OperationOutcome resource
|
|
159
|
+
*/
|
|
160
|
+
operationOutcome(severity, code, message) {
|
|
161
|
+
return {
|
|
162
|
+
resourceType: 'OperationOutcome',
|
|
163
|
+
issue: [{
|
|
164
|
+
severity,
|
|
165
|
+
code,
|
|
166
|
+
diagnostics: message
|
|
167
|
+
}]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async readValueSet(res, prefix, params) {
|
|
172
|
+
const valueSetParam = this.findParameter(params, prefix+'ValueSet');
|
|
173
|
+
if (valueSetParam && valueSetParam.resource) {
|
|
174
|
+
let valueSet = new ValueSet(valueSetParam.resource);
|
|
175
|
+
this.seeSourceVS(valueSet);
|
|
176
|
+
return valueSet;
|
|
177
|
+
}
|
|
178
|
+
// If no valueSet yet, try to find by url
|
|
179
|
+
const urlParam = this.findParameter(params, prefix+'Url');
|
|
180
|
+
const versionParam = this.findParameter(params, 'valueSetVersion');
|
|
181
|
+
|
|
182
|
+
if (!urlParam) {
|
|
183
|
+
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
184
|
+
`Must provide either a ${prefix}ValueSet resource or a ${prefix}Url parameter`));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const url = this.getParameterValue(urlParam);
|
|
188
|
+
const version = versionParam ? this.getParameterValue(versionParam) : null;
|
|
189
|
+
|
|
190
|
+
let valueSet = await this.findValueSet(url, version);
|
|
191
|
+
this.seeSourceVS(valueSet, url);
|
|
192
|
+
if (!valueSet) {
|
|
193
|
+
return res.status(404).json(this.operationOutcome('error', 'not-found',
|
|
194
|
+
version ? `ValueSet not found: ${url} version ${version}` : `ValueSet not found: ${url}`));
|
|
195
|
+
} else {
|
|
196
|
+
return valueSet;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async doRelated(txp, thisVS, otherVS) {
|
|
201
|
+
// ok, we have to compare the composes. we don't care about anything else
|
|
202
|
+
const thisC = thisVS.jsonObj.compose;
|
|
203
|
+
const otherC = otherVS.jsonObj.compose;
|
|
204
|
+
if (!thisC) {
|
|
205
|
+
return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`);
|
|
206
|
+
}
|
|
207
|
+
Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose')
|
|
208
|
+
this.checkNoLockedDate(thisVS.vurl, thisC);
|
|
209
|
+
if (!otherC) {
|
|
210
|
+
return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`);
|
|
211
|
+
}
|
|
212
|
+
Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose')
|
|
213
|
+
this.checkNoLockedDate(otherVS.vurl, otherC);
|
|
214
|
+
|
|
215
|
+
let systems = new Map(); // tracks whether they are version dependent or not
|
|
216
|
+
|
|
217
|
+
// ok, first, if we can determine that the value sets match from the definitions, we will
|
|
218
|
+
// if that fails, then we have to do the expansions, and then decide
|
|
219
|
+
|
|
220
|
+
// first, we sort the includes by system, and then compare them as a group
|
|
221
|
+
// Build a map of system -> { this: [...includes], other: [...includes] }
|
|
222
|
+
const systemMap = new Map();
|
|
223
|
+
await this.addIncludes(systems, systemMap, thisC.include || [], 'this', txp);
|
|
224
|
+
await this.addIncludes(systems, systemMap, otherC.include || [], 'other', txp);
|
|
225
|
+
await this.addIncludes(systems, systemMap, thisC.exclude || [], 'thisEx', txp);
|
|
226
|
+
await this.addIncludes(systems, systemMap, otherC.exclude || [], 'otherEx', txp);
|
|
227
|
+
|
|
228
|
+
let status = { left: false, right: false, fail: false, common : false};
|
|
229
|
+
|
|
230
|
+
for (const [key, value] of systemMap.entries()) {
|
|
231
|
+
if (key) {
|
|
232
|
+
let cs = await this.findCodeSystem(key, null, txp, ['complete', 'fragment'], null, true);
|
|
233
|
+
await this.compareSystems(systems, status, cs, value);
|
|
234
|
+
} else {
|
|
235
|
+
this.compareNonSystems(status, value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let exp = false;
|
|
240
|
+
// can't tell? OK, we need to do expansions. Note that
|
|
241
|
+
// expansions might not work (infinite value sets) so
|
|
242
|
+
// we can't tell.
|
|
243
|
+
if (status.fail) {
|
|
244
|
+
status.fail = false;
|
|
245
|
+
exp = true;
|
|
246
|
+
await this.compareExpansions(systems, status, thisVS, otherVS);
|
|
247
|
+
}
|
|
248
|
+
let outcome;
|
|
249
|
+
if (status.fail) {
|
|
250
|
+
outcome = this.makeOutcome("indeterminate", `Unable to compare ${thisVS.vurl} and ${otherVS.vurl}: `+status.reason);
|
|
251
|
+
} else if (!status.common) {
|
|
252
|
+
outcome = this.makeOutcome("disjoint", `No shared codes between the value sets ${thisVS.vurl} and ${otherVS.vurl}`);
|
|
253
|
+
} else if (!status.left && !status.right) {
|
|
254
|
+
outcome = this.makeOutcome("same", `The value sets ${thisVS.vurl} and ${otherVS.vurl} contain the same codes`);
|
|
255
|
+
} else if (status.left && status.right) {
|
|
256
|
+
outcome = this.makeOutcome("overlapping", `Both value sets ${thisVS.vurl} and ${otherVS.vurl} contain the codes the other doesn't, but there is some overlap`);
|
|
257
|
+
} else if (status.left) {
|
|
258
|
+
outcome = this.makeOutcome("superset", `The valueSet ${thisVS.vurl} is a super-set of the valueSet ${otherVS.vurl}`);
|
|
259
|
+
} else {
|
|
260
|
+
outcome = this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a seb-set of the valueSet ${otherVS.vurl}`);
|
|
261
|
+
}
|
|
262
|
+
if (exp) {
|
|
263
|
+
outcome.parameter.push({name: 'expansion', valueBoolean: exp})
|
|
264
|
+
}
|
|
265
|
+
return outcome;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async addIncludes(systems, systemMap, includes, side, txp) {
|
|
269
|
+
for (const inc of includes) {
|
|
270
|
+
let key = inc.system || '';
|
|
271
|
+
let v = {};
|
|
272
|
+
if (await this.versionMatters(systems, key, inc.version, v, txp)) {
|
|
273
|
+
key = key + "|" + v.version;
|
|
274
|
+
}
|
|
275
|
+
if (!systemMap.has(key)) {
|
|
276
|
+
systemMap.set(key, {this: [], other: []});
|
|
277
|
+
}
|
|
278
|
+
systemMap.get(key)[side].push(inc);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async versionMatters(systems, key, version, v, txp) {
|
|
283
|
+
if (systems.has(key)) {
|
|
284
|
+
return systems.get(key);
|
|
285
|
+
}
|
|
286
|
+
let cs = await this.findCodeSystem(key, version, txp, ['complete', 'fragment'], null, true);
|
|
287
|
+
let res = cs == null || cs.versionNeeded();
|
|
288
|
+
if (res) {
|
|
289
|
+
v.version = version || cs ? cs.version() : undefined;
|
|
290
|
+
}
|
|
291
|
+
systems.set(key, res);
|
|
292
|
+
return res;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
compareNonSystems(status) {
|
|
296
|
+
// not done yet
|
|
297
|
+
status.fail = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async compareSystems(systems, status, cs, value) {
|
|
301
|
+
if (value.thisEx || value.otherEx) {
|
|
302
|
+
// we don't try in this case
|
|
303
|
+
status.fail = true;
|
|
304
|
+
status.common = true;
|
|
305
|
+
} else if (!value.this) {
|
|
306
|
+
// left has nothing for this one.
|
|
307
|
+
status.right = true;
|
|
308
|
+
status.common = true;
|
|
309
|
+
} else if (!value.other) {
|
|
310
|
+
status.left = true;
|
|
311
|
+
status.common = true;
|
|
312
|
+
} else {
|
|
313
|
+
// for now, we don't do value set imports
|
|
314
|
+
if (this.hasValueSets(value.this) || this.hasValueSets(value.other)) {
|
|
315
|
+
status.fail = true;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (this.hasConceptsAndFilters(value.this) || this.hasConceptsAndFilters(value.other)) {
|
|
319
|
+
status.fail = true;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// we have includes on both sides. We might have full system, a list, or a filter. we don't care about order. so clean up and sort
|
|
323
|
+
this.tidyIncludes(value.this);
|
|
324
|
+
this.tidyIncludes(value.other);
|
|
325
|
+
if (!value.this || value.this.length === 0) {
|
|
326
|
+
status.right = true;
|
|
327
|
+
return;
|
|
328
|
+
} else if (!value.other || value.other.length === 0) {
|
|
329
|
+
status.left = true;
|
|
330
|
+
return;
|
|
331
|
+
} else if (this.isFullSystem(value.this[0]) && this.isFullSystem(value.other[0])) {
|
|
332
|
+
// if both sides have full include, they match, period.
|
|
333
|
+
status.common = true;
|
|
334
|
+
return;
|
|
335
|
+
} else if (this.isFullSystem(value.this[0])) {
|
|
336
|
+
status.common = true;
|
|
337
|
+
status.left = true;
|
|
338
|
+
return;
|
|
339
|
+
} else if (this.isFullSystem(value.other[0])) {
|
|
340
|
+
status.common = true;
|
|
341
|
+
status.right = true;
|
|
342
|
+
return;
|
|
343
|
+
} else if (this.isConcepts(value.this[0]) && this.isConcepts(value.other[0])) {
|
|
344
|
+
this.compareCodeLists(status, value.this[0], value.other[0]);
|
|
345
|
+
return;
|
|
346
|
+
} else if (this.isFilter(value.this[0]) && this.isFilter(value.other[0])) {
|
|
347
|
+
if (value.this.length != value.other.length) {
|
|
348
|
+
status.fail = true;
|
|
349
|
+
return;
|
|
350
|
+
} else {
|
|
351
|
+
for (let i = 0; i < value.this.length; i++) {
|
|
352
|
+
let t = value.this[i];
|
|
353
|
+
let o = value.other[i];
|
|
354
|
+
if (!await this.filterSetsMatch(status, cs, t, o)) {
|
|
355
|
+
status.fail = true;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
status.common = true;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
status.fail = true; // not sure why we got to here, but it doesn't matter: we can't tell
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
hasValueSets(list) {
|
|
368
|
+
for (const inc of list) {
|
|
369
|
+
if (inc.valueSet) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
hasConceptsAndFilters(list) {
|
|
377
|
+
for (const inc of list) {
|
|
378
|
+
if (inc.concept?.length > 0 && inc.filter?.length > 0) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
tidyIncludes(list) {
|
|
386
|
+
let collector = null;
|
|
387
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
388
|
+
const inc = list[i];
|
|
389
|
+
if (inc.system && inc.concept && !inc.filter) {
|
|
390
|
+
if (collector) {
|
|
391
|
+
collector.concept.push(...inc.concept);
|
|
392
|
+
list.splice(i, 1);
|
|
393
|
+
} else {
|
|
394
|
+
collector = inc;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (let inc of list) {
|
|
399
|
+
if (inc.concept) {
|
|
400
|
+
inc.concept.sort((a, b) => (a.code || '').localeCompare(b.code));
|
|
401
|
+
}
|
|
402
|
+
if (inc.filter) {
|
|
403
|
+
inc.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function includeRank(inc) {
|
|
407
|
+
if (!inc.system) return 0;
|
|
408
|
+
const hasConcepts = inc.concept?.length > 0;
|
|
409
|
+
const hasFilters = inc.filter?.length > 0;
|
|
410
|
+
if (!hasConcepts && !hasFilters) return 1;
|
|
411
|
+
if (hasConcepts && !hasFilters) return 2;
|
|
412
|
+
if (!hasConcepts && hasFilters) return 3;
|
|
413
|
+
return 4;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function compareFilter(a, b) {
|
|
417
|
+
const af = a.filter?.[0];
|
|
418
|
+
const bf = b.filter?.[0];
|
|
419
|
+
if (!af && !bf) return 0;
|
|
420
|
+
if (!af) return -1;
|
|
421
|
+
if (!bf) return 1;
|
|
422
|
+
return (af.property || '').localeCompare(bf.property || '') ||
|
|
423
|
+
(af.op || '').localeCompare(bf.op || '') ||
|
|
424
|
+
(af.value || '').localeCompare(bf.value || '');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
list.sort((a, b) =>
|
|
428
|
+
includeRank(a) - includeRank(b) ||
|
|
429
|
+
compareFilter(a, b)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
compareCodeLists(status, t, o) {
|
|
434
|
+
const tSet = new Set(t.concept.map(x => x.code));
|
|
435
|
+
const oSet = new Set(o.concept.map(x => x.code));
|
|
436
|
+
|
|
437
|
+
status.common = [...tSet].filter(c => oSet.has(c)).length > 0;
|
|
438
|
+
status.left = [...tSet].filter(c => !oSet.has(c)).length > 0;
|
|
439
|
+
status.right = [...oSet].filter(c => !tSet.has(c)).length > 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
makeOutcome(code, msg) {
|
|
443
|
+
const parameters = {
|
|
444
|
+
resourceType: 'Parameters',
|
|
445
|
+
parameter: [
|
|
446
|
+
{name: 'result', valueCode: code}
|
|
447
|
+
]
|
|
448
|
+
};
|
|
449
|
+
if (msg) {
|
|
450
|
+
parameters.parameter.push({name: 'message', valueString: msg})
|
|
451
|
+
}
|
|
452
|
+
return parameters;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
isFullSystem(inc) {
|
|
456
|
+
return !inc.concept && !inc.filter;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async compareExpansions(systems, status, thisC, otherC) {
|
|
460
|
+
const expThis = await this.doExpand(thisC);
|
|
461
|
+
const expOther = await this.doExpand(otherC);
|
|
462
|
+
|
|
463
|
+
if (this.isUnclosed(expThis) || this.isUnclosed(expOther)) {
|
|
464
|
+
status.fail = true;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const matcher = new ArrayMatcher((l, r) =>
|
|
469
|
+
this.matchContains(systems, l, r)
|
|
470
|
+
);
|
|
471
|
+
await matcher.match(expThis.expansion.contains, expOther.expansion.contains);
|
|
472
|
+
if (!expThis.expansion.contains) {
|
|
473
|
+
expThis.expansion.contains = [];
|
|
474
|
+
}
|
|
475
|
+
if (matcher.matched.length > 0) {
|
|
476
|
+
status.common = true;
|
|
477
|
+
}
|
|
478
|
+
if (matcher.unmatchedLeft.length > 0) {
|
|
479
|
+
status.left = true;
|
|
480
|
+
}
|
|
481
|
+
if (matcher.unmatchedRight.length > 0) {
|
|
482
|
+
status.right = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
isUnclosed(vs) {
|
|
487
|
+
return Extensions.has(vs.expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
matchContains(systems, thisC, otherC) {
|
|
491
|
+
if (thisC.system != otherC.system) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
if (thisC.code != otherC.code) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
let versionMatters = systems.get(thisC.system);
|
|
498
|
+
if (versionMatters && thisC.version != otherC.version) {
|
|
499
|
+
return false;
|
|
500
|
+
} else {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async doExpand(vs) {
|
|
506
|
+
let txpe = this.params.clone();
|
|
507
|
+
txpe.limit = 10000;
|
|
508
|
+
txpe.excludeNested = true;
|
|
509
|
+
let exp = new ValueSetExpander(this, txpe);
|
|
510
|
+
let vse = await exp.expand(vs, new SearchFilterText(''), true);
|
|
511
|
+
return vse
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
isConcepts(inc) {
|
|
515
|
+
return inc.concept && inc.concept.length > 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
isFilter(inc) {
|
|
519
|
+
return inc.filter && inc.filter.length > 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async filterSetsMatch(status, cs, t, o) {
|
|
523
|
+
// two includes have matching filters if the set of filters match.
|
|
524
|
+
|
|
525
|
+
let localstatus = { left: false, right: false};
|
|
526
|
+
|
|
527
|
+
const matcher = new ArrayMatcher((l, r) =>
|
|
528
|
+
this.filtersMatch(localstatus, cs, l, r)
|
|
529
|
+
);
|
|
530
|
+
await matcher.match(t.filter, o.filter);
|
|
531
|
+
|
|
532
|
+
if (matcher.unmatchedLeft.length > 0 || matcher.unmatchedRight.length > 0) {
|
|
533
|
+
return false;
|
|
534
|
+
} else {
|
|
535
|
+
if (localstatus.left) {
|
|
536
|
+
status.left = true;
|
|
537
|
+
}
|
|
538
|
+
if (localstatus.right) {
|
|
539
|
+
status.right = true;
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async filtersMatch(status, cs, t, o) {
|
|
546
|
+
if (t.property != o.property || t.op != o.op) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (t.value == o.value) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
if (t.op == 'is-a') {
|
|
553
|
+
let rel = await cs.subsumesTest(t.value, o.value)
|
|
554
|
+
switch (rel) {
|
|
555
|
+
case 'equivalent':
|
|
556
|
+
return true;
|
|
557
|
+
case 'subsumes':
|
|
558
|
+
status.left = true;
|
|
559
|
+
return true;
|
|
560
|
+
case 'subsumed-by':
|
|
561
|
+
status.right = true;
|
|
562
|
+
return true;
|
|
563
|
+
default:
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = {
|
|
573
|
+
RelatedWorker
|
|
574
|
+
};
|
package/tx/workers/search.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
|
|
8
8
|
const { TerminologyWorker } = require('./worker');
|
|
9
|
+
const {Utilities} = require("../../library/utilities");
|
|
9
10
|
|
|
10
11
|
class SearchWorker extends TerminologyWorker {
|
|
11
12
|
/**
|
|
@@ -30,12 +31,16 @@ class SearchWorker extends TerminologyWorker {
|
|
|
30
31
|
|
|
31
32
|
// Allowed search parameters
|
|
32
33
|
static ALLOWED_PARAMS = [
|
|
33
|
-
'_offset', '_count', '_elements', '_sort',
|
|
34
|
+
'_offset', '_count', '_elements', '_sort', '_summary', '_total',
|
|
34
35
|
'url', 'version', 'content-mode', 'date', 'description',
|
|
35
36
|
'supplements', 'identifier', 'jurisdiction', 'name',
|
|
36
37
|
'publisher', 'status', 'system', 'title', 'text'
|
|
37
38
|
];
|
|
38
39
|
|
|
40
|
+
// Summary elements for _summary=true (common metadata fields)
|
|
41
|
+
static SUMMARY_ELEMENTS = ['resourceType', 'id', 'meta', 'url', 'version',
|
|
42
|
+
'name', 'title', 'status', 'date', 'publisher', 'description'];
|
|
43
|
+
|
|
39
44
|
// Sortable fields
|
|
40
45
|
static SORT_FIELDS = ['id', 'url', 'version', 'date', 'name', 'vurl'];
|
|
41
46
|
|
|
@@ -54,8 +59,27 @@ class SearchWorker extends TerminologyWorker {
|
|
|
54
59
|
try {
|
|
55
60
|
// Parse pagination parameters
|
|
56
61
|
const offset = Math.max(0, parseInt(params._offset) || 0);
|
|
57
|
-
const
|
|
58
|
-
const
|
|
62
|
+
const summary = params._summary || 'false';
|
|
63
|
+
const totalMode = params._total || 'accurate';
|
|
64
|
+
|
|
65
|
+
// Determine elements based on _summary parameter
|
|
66
|
+
let elements;
|
|
67
|
+
switch (summary) {
|
|
68
|
+
case 'true':
|
|
69
|
+
elements = SearchWorker.SUMMARY_ELEMENTS;
|
|
70
|
+
break;
|
|
71
|
+
case 'text':
|
|
72
|
+
elements = ['resourceType', 'id', 'meta', 'text'];
|
|
73
|
+
break;
|
|
74
|
+
case 'data':
|
|
75
|
+
elements = null; // no filter for terminology
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : 20);
|
|
59
83
|
const sort = params._sort || "id";
|
|
60
84
|
|
|
61
85
|
// Get matching resources
|
|
@@ -83,14 +107,15 @@ class SearchWorker extends TerminologyWorker {
|
|
|
83
107
|
|
|
84
108
|
// Build and return the bundle
|
|
85
109
|
const bundle = this.buildSearchBundle(
|
|
86
|
-
req, resourceType, matches, offset, count, elements
|
|
110
|
+
req, resourceType, matches, offset, count, elements, summary, totalMode
|
|
87
111
|
);
|
|
88
|
-
req.logInfo = `${bundle.entry.length} matches`;
|
|
112
|
+
req.logInfo = summary === 'count' ? `count: ${bundle.total}` : `${bundle.entry.length} matches`;
|
|
89
113
|
return res.json(bundle);
|
|
90
114
|
|
|
91
115
|
} catch (error) {
|
|
92
|
-
req.logInfo = "error "+(error.msgId || error.className);
|
|
93
116
|
this.log.error(error);
|
|
117
|
+
this.debugLog(error);
|
|
118
|
+
req.logInfo = "error "+(error.msgId || error.className);
|
|
94
119
|
return res.status(500).json({
|
|
95
120
|
resourceType: 'OperationOutcome',
|
|
96
121
|
issue: [{
|
|
@@ -266,9 +291,18 @@ class SearchWorker extends TerminologyWorker {
|
|
|
266
291
|
/**
|
|
267
292
|
* Build a FHIR search Bundle with pagination
|
|
268
293
|
*/
|
|
269
|
-
buildSearchBundle(req, resourceType, allMatches, offset, count, elements) {
|
|
294
|
+
buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary = 'false', totalMode = 'accurate') {
|
|
270
295
|
const total = allMatches.length;
|
|
271
296
|
|
|
297
|
+
// For _summary=count, return just the count
|
|
298
|
+
if (summary === 'count') {
|
|
299
|
+
return {
|
|
300
|
+
resourceType: 'Bundle',
|
|
301
|
+
type: 'searchset',
|
|
302
|
+
total: total
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
272
306
|
// Get the slice for this page
|
|
273
307
|
const pageResults = allMatches.slice(offset, offset + count);
|
|
274
308
|
|
|
@@ -352,13 +386,16 @@ class SearchWorker extends TerminologyWorker {
|
|
|
352
386
|
};
|
|
353
387
|
});
|
|
354
388
|
|
|
355
|
-
|
|
389
|
+
const bundle = {
|
|
356
390
|
resourceType: 'Bundle',
|
|
357
391
|
type: 'searchset',
|
|
358
|
-
total: total,
|
|
359
392
|
link: links,
|
|
360
393
|
entry: entries
|
|
361
394
|
};
|
|
395
|
+
if (totalMode !== 'none') {
|
|
396
|
+
bundle.total = total;
|
|
397
|
+
}
|
|
398
|
+
return bundle;
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
/**
|