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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +53 -5
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. 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
+ };
@@ -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 elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null;
58
- const count = Math.min(elements ? 2000 : 200, Math.max(1, parseInt(params._count) || 20));
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
- return {
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
  /**