fhirsmith 0.8.4 → 0.8.6
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 +42 -0
- package/README.md +52 -22
- package/extension-tracker/extension-tracker-template.html +3 -1
- package/library/html-server.js +7 -0
- package/library/logger.js +234 -194
- package/package.json +6 -2
- package/packages/packages-template.html +3 -1
- package/publisher/publisher-template.html +1 -0
- package/registry/registry-template.html +3 -1
- package/root-template.html +3 -2
- package/server.js +48 -12
- package/static/assets/js/clipboard-btn.js +31 -0
- package/static/assets/js/clipboard.min.js +7 -0
- package/static/fhir.css +20 -1
- package/static/images/noun_copy to clipboard_1669410.png +0 -0
- package/translations/Messages.properties +1 -1
- package/translations/rendering-phrases.properties +146 -55
- package/tx/cs/cs-api.js +12 -0
- package/tx/cs/cs-cs.js +5 -1
- package/tx/cs/cs-snomed.js +36 -12
- package/tx/html/tx-template.html +8 -4
- package/tx/library/codesystem.js +4 -0
- package/tx/library/renderer.js +46 -12
- package/tx/operation-context.js +74 -19
- package/tx/tx-html.js +5 -5
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/tx.js +1 -0
- package/tx/workers/expand.js +149 -112
- package/tx/workers/metadata.js +3 -2
- package/tx/workers/read.js +10 -3
- package/tx/workers/related.js +228 -87
- package/tx/workers/search.js +126 -51
- package/tx/workers/translate.js +3 -3
- package/xig/xig-template.html +3 -1
- package/library/logger-telnet.js +0 -205
package/tx/workers/related.js
CHANGED
|
@@ -18,8 +18,9 @@ const {SearchFilterText} = require("../library/designations");
|
|
|
18
18
|
const {ArrayMatcher} = require("../../library/utilities");
|
|
19
19
|
const {debugLog} = require("../operation-context");
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
class RelatedWorker extends TerminologyWorker {
|
|
22
|
+
showLogic = false;
|
|
23
|
+
|
|
23
24
|
/**
|
|
24
25
|
* @param {OperationContext} opContext - Operation context
|
|
25
26
|
* @param {Logger} log - Logger instance
|
|
@@ -115,7 +116,6 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
115
116
|
this.setupAdditionalResources(params);
|
|
116
117
|
let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false);
|
|
117
118
|
txp.readParams(params);
|
|
118
|
-
|
|
119
119
|
this.params = txp;
|
|
120
120
|
|
|
121
121
|
let thisVS = await this.readValueSet(res, "this", params, txp);
|
|
@@ -199,6 +199,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
async doRelated(txp, thisVS, otherVS) {
|
|
202
|
+
|
|
202
203
|
// ok, we have to compare the composes. we don't care about anything else
|
|
203
204
|
const thisC = thisVS.jsonObj.compose;
|
|
204
205
|
const otherC = otherVS.jsonObj.compose;
|
|
@@ -213,28 +214,35 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
213
214
|
Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose', otherVS.vurl)
|
|
214
215
|
this.checkNoLockedDate(otherVS.vurl, otherC);
|
|
215
216
|
|
|
216
|
-
let systems = new Map(); // tracks whether
|
|
217
|
+
let systems = new Map(); // tracks whether the comparison is version dependent or not
|
|
217
218
|
|
|
218
219
|
// ok, first, if we can determine that the value sets match from the definitions, we will
|
|
219
220
|
// if that fails, then we have to do the expansions, and then decide
|
|
220
221
|
|
|
222
|
+
let allCriteria = [...thisC.include || [], ...thisC.exclude || [], ...otherC.include || [], ...otherC.exclude || []];
|
|
221
223
|
// first, we sort the includes by system, and then compare them as a group
|
|
222
224
|
// Build a map of system -> { this: [...includes], other: [...includes] }
|
|
223
225
|
const systemMap = new Map();
|
|
224
|
-
await this.addIncludes(systems, systemMap, thisC.include || [], 'this', txp);
|
|
225
|
-
await this.addIncludes(systems, systemMap, otherC.include || [], 'other', txp);
|
|
226
|
-
await this.addIncludes(systems, systemMap, thisC.exclude || [], 'thisEx', txp);
|
|
227
|
-
await this.addIncludes(systems, systemMap, otherC.exclude || [], 'otherEx', txp);
|
|
228
|
-
|
|
229
|
-
let status = { left: false, right: false, fail: false, common : false};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
226
|
+
await this.addIncludes(systems, systemMap, thisC.include || [], 'this', txp, allCriteria);
|
|
227
|
+
await this.addIncludes(systems, systemMap, otherC.include || [], 'other', txp, allCriteria);
|
|
228
|
+
await this.addIncludes(systems, systemMap, thisC.exclude || [], 'thisEx', txp, allCriteria);
|
|
229
|
+
await this.addIncludes(systems, systemMap, otherC.exclude || [], 'otherEx', txp, allCriteria);
|
|
230
|
+
|
|
231
|
+
let status = { empty: false, left: false, right: false, fail: false, common : false};
|
|
232
|
+
let diagnostics = {};
|
|
233
|
+
|
|
234
|
+
let canBeQuick = !this.hasMultipleVersionsForAnySystem(systems, systemMap);
|
|
235
|
+
if (canBeQuick) {
|
|
236
|
+
for (const [key, value] of systemMap.entries()) {
|
|
237
|
+
if (key) {
|
|
238
|
+
let cs = await this.findCodeSystem(key, null, txp, ['complete', 'fragment'], null, true);
|
|
239
|
+
await this.compareSystems(systems, status, cs, value, diagnostics);
|
|
240
|
+
} else {
|
|
241
|
+
this.compareNonSystems(status, value, diagnostics);
|
|
242
|
+
}
|
|
237
243
|
}
|
|
244
|
+
} else {
|
|
245
|
+
status.fail = true;
|
|
238
246
|
}
|
|
239
247
|
|
|
240
248
|
let exp = false;
|
|
@@ -242,13 +250,15 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
242
250
|
// expansions might not work (infinite value sets) so
|
|
243
251
|
// we can't tell.
|
|
244
252
|
if (status.fail) {
|
|
245
|
-
status
|
|
253
|
+
status = { left: false, right: false, fail: false, common : false}; // reset;
|
|
246
254
|
exp = true;
|
|
247
|
-
await this.compareExpansions(systems, status, thisVS, otherVS);
|
|
255
|
+
await this.compareExpansions(systems, status, thisVS, otherVS, diagnostics);
|
|
248
256
|
}
|
|
249
257
|
let outcome;
|
|
250
258
|
if (status.fail) {
|
|
251
259
|
outcome = this.makeOutcome("indeterminate", `Unable to compare ${thisVS.vurl} and ${otherVS.vurl}: `+status.reason);
|
|
260
|
+
} else if (status.empty) {
|
|
261
|
+
outcome = this.makeOutcome("empty", `Both the value sets ${thisVS.vurl} and ${otherVS.vurl} are empty`);
|
|
252
262
|
} else if (!status.common) {
|
|
253
263
|
outcome = this.makeOutcome("disjoint", `No shared codes between the value sets ${thisVS.vurl} and ${otherVS.vurl}`);
|
|
254
264
|
} else if (!status.left && !status.right) {
|
|
@@ -260,36 +270,56 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
260
270
|
} else {
|
|
261
271
|
outcome = this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a seb-set of the valueSet ${otherVS.vurl}`);
|
|
262
272
|
}
|
|
263
|
-
if (
|
|
264
|
-
outcome.parameter.push({name: 'expansion', valueBoolean: exp})
|
|
273
|
+
if (txp.diagnostics) {
|
|
274
|
+
outcome.parameter.push({name: 'performed-expansion', valueBoolean: exp ? true : false})
|
|
275
|
+
if (diagnostics.missing && diagnostics.missing.length > 0) {
|
|
276
|
+
outcome.parameter.push({name: 'missing-codes', valueString: diagnostics.missing.map(c => c.code).join(',') })
|
|
277
|
+
}
|
|
278
|
+
if (diagnostics.extra && diagnostics.extra.length > 0) {
|
|
279
|
+
outcome.parameter.push({name: 'extra-codes', valueString: diagnostics.extra.map(c => c.code).join(',') })
|
|
280
|
+
}
|
|
281
|
+
if (diagnostics.common && diagnostics.common.length > 0) {
|
|
282
|
+
outcome.parameter.push({name: 'common-codes', valueString: diagnostics.common.map(c => c.left.code).join(',') })
|
|
283
|
+
}
|
|
284
|
+
if (!exp) {
|
|
285
|
+
if (diagnostics.missingCodes && diagnostics.missingCodes.length > 0) {
|
|
286
|
+
outcome.parameter.push({name: 'missing-codes', valueString: diagnostics.missingCodes.join(',')})
|
|
287
|
+
}
|
|
288
|
+
if (diagnostics.extraCodes && diagnostics.extraCodes.length > 0) {
|
|
289
|
+
outcome.parameter.push({name: 'extra-codes', valueString: diagnostics.extraCodes.join(',')})
|
|
290
|
+
}
|
|
291
|
+
if (diagnostics.commonCodes && diagnostics.commonCodes.length > 0) {
|
|
292
|
+
outcome.parameter.push({name: 'common-codes', valueString: diagnostics.commonCodes.join(',')})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
265
295
|
}
|
|
266
296
|
return outcome;
|
|
267
297
|
}
|
|
268
298
|
|
|
269
|
-
async addIncludes(systems, systemMap, includes, side, txp) {
|
|
299
|
+
async addIncludes(systems, systemMap, includes, side, txp, allCriteria) {
|
|
270
300
|
for (const inc of includes) {
|
|
271
301
|
let key = inc.system || '';
|
|
272
302
|
let v = {};
|
|
273
|
-
if (await this.versionMatters(systems, key, inc.version, v, txp)) {
|
|
303
|
+
if (await this.versionMatters(systems, key, inc.version, v, txp, allCriteria)) {
|
|
274
304
|
key = key + "|" + v.version;
|
|
275
305
|
}
|
|
276
306
|
if (!systemMap.has(key)) {
|
|
277
|
-
systemMap.set(key, {this: [], other: []});
|
|
307
|
+
systemMap.set(key, {this: [], other: [], thisEx: [], otherEx: []});
|
|
278
308
|
}
|
|
279
309
|
systemMap.get(key)[side].push(inc);
|
|
280
310
|
}
|
|
281
311
|
}
|
|
282
312
|
|
|
283
|
-
async versionMatters(systems, key, version, v, txp) {
|
|
284
|
-
if (systems.has(key)) {
|
|
285
|
-
return systems.get(key);
|
|
286
|
-
}
|
|
313
|
+
async versionMatters(systems, key, version, v, txp, allCriteria) {
|
|
287
314
|
let cs = await this.findCodeSystem(key, version, txp, ['complete', 'fragment'], null, true);
|
|
288
|
-
let
|
|
315
|
+
let alreadyVersionDependent = systems.has(key) && systems.get(key).criteria;
|
|
316
|
+
let res = cs != null && (alreadyVersionDependent || ((version || cs.version()) && (cs.versionNeeded() || this.anyCriteriaHasFilters(allCriteria, key)))); // if there's filters, the version always matters
|
|
289
317
|
if (res) {
|
|
290
318
|
v.version = version || cs ? cs.version() : undefined;
|
|
291
319
|
}
|
|
292
|
-
systems.
|
|
320
|
+
if (!systems.has(key)) {
|
|
321
|
+
systems.set(key, {criteria: res, codes: cs ? cs.versionNeeded() : false});
|
|
322
|
+
}
|
|
293
323
|
return res;
|
|
294
324
|
}
|
|
295
325
|
|
|
@@ -298,8 +328,8 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
298
328
|
status.fail = true;
|
|
299
329
|
}
|
|
300
330
|
|
|
301
|
-
async compareSystems(systems, status, cs, value) {
|
|
302
|
-
if (value.thisEx || value.otherEx) {
|
|
331
|
+
async compareSystems(systems, status, cs, value, diagnostics) {
|
|
332
|
+
if ((value.thisEx && value.thisEx.length > 0) || (value.otherEx && value.otherEx.length > 0)) {
|
|
303
333
|
// we don't try in this case
|
|
304
334
|
status.fail = true;
|
|
305
335
|
status.common = true;
|
|
@@ -341,25 +371,33 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
341
371
|
status.common = true;
|
|
342
372
|
status.right = true;
|
|
343
373
|
return;
|
|
344
|
-
} else if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
} else if (value.this.length > 1 || value.other.length > 1) {
|
|
375
|
+
status.common = true;
|
|
376
|
+
// if we have mixed concepts, or multiple filters, we can't reason about them (too many scenarios where they overlap in
|
|
377
|
+
// unpredictable ways. If they're not identical, we fail
|
|
348
378
|
if (value.this.length != value.other.length) {
|
|
349
379
|
status.fail = true;
|
|
350
|
-
return;
|
|
351
380
|
} else {
|
|
352
381
|
for (let i = 0; i < value.this.length; i++) {
|
|
353
382
|
let t = value.this[i];
|
|
354
383
|
let o = value.other[i];
|
|
355
|
-
if (!
|
|
384
|
+
if (!this.includesIdentical(t, o)) {
|
|
356
385
|
status.fail = true;
|
|
357
|
-
|
|
386
|
+
break;
|
|
358
387
|
}
|
|
359
|
-
status.common = true;
|
|
360
|
-
return;
|
|
361
388
|
}
|
|
362
389
|
}
|
|
390
|
+
return;
|
|
391
|
+
} else if (this.isConcepts(value.this[0]) && this.isConcepts(value.other[0])) {
|
|
392
|
+
this.compareCodeLists(status, value.this[0], value.other[0], diagnostics);
|
|
393
|
+
return;
|
|
394
|
+
} else if (this.isFilter(value.this[0]) && this.isFilter(value.other[0])) {
|
|
395
|
+
let t = value.this[0];
|
|
396
|
+
let o = value.other[0];
|
|
397
|
+
if (!await this.filterSetsMatch(status, cs, t, o)) {
|
|
398
|
+
status.fail = true;
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
363
401
|
}
|
|
364
402
|
}
|
|
365
403
|
status.fail = true; // not sure why we got to here, but it doesn't matter: we can't tell
|
|
@@ -383,6 +421,15 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
383
421
|
return false;
|
|
384
422
|
}
|
|
385
423
|
|
|
424
|
+
hasFilters(list) {
|
|
425
|
+
for (const inc of list) {
|
|
426
|
+
if (inc.filter?.length > 0) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
386
433
|
tidyIncludes(list) {
|
|
387
434
|
let collector = null;
|
|
388
435
|
for (let i = list.length - 1; i >= 0; i--) {
|
|
@@ -431,13 +478,16 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
431
478
|
);
|
|
432
479
|
}
|
|
433
480
|
|
|
434
|
-
compareCodeLists(status, t, o) {
|
|
481
|
+
compareCodeLists(status, t, o, diagnostics) {
|
|
435
482
|
const tSet = new Set(t.concept.map(x => x.code));
|
|
436
483
|
const oSet = new Set(o.concept.map(x => x.code));
|
|
437
484
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
485
|
+
diagnostics.commonCodes = [...tSet].filter(c => oSet.has(c));
|
|
486
|
+
diagnostics.missingCodes = [...tSet].filter(c => !oSet.has(c));
|
|
487
|
+
diagnostics.extraCodes = [...oSet].filter(c => !tSet.has(c));
|
|
488
|
+
status.common = diagnostics.commonCodes.length > 0;
|
|
489
|
+
status.left = diagnostics.missingCodes.length > 0;
|
|
490
|
+
status.right =diagnostics.extraCodes.length > 0;
|
|
441
491
|
}
|
|
442
492
|
|
|
443
493
|
makeOutcome(code, msg) {
|
|
@@ -457,15 +507,45 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
457
507
|
return !inc.concept && !inc.filter;
|
|
458
508
|
}
|
|
459
509
|
|
|
460
|
-
async compareExpansions(systems, status, thisC, otherC) {
|
|
461
|
-
|
|
462
|
-
const
|
|
510
|
+
async compareExpansions(systems, status, thisC, otherC, diagnostics) {
|
|
511
|
+
|
|
512
|
+
const expResThis = await this.doExpand(thisC);
|
|
513
|
+
this.opContext.unSeeAll();
|
|
514
|
+
const expResOther = await this.doExpand(otherC);
|
|
463
515
|
|
|
516
|
+
if (expResThis.error || expResOther.error) {
|
|
517
|
+
status.fail = true;
|
|
518
|
+
if (expResThis.error && expResOther.error) {
|
|
519
|
+
if (expResThis.error == expResOther.error) {
|
|
520
|
+
status.reason = "Both expansions failed: "+expResThis.error.message;
|
|
521
|
+
} else {
|
|
522
|
+
status.reason = "Both expansions failed with different errors: "+expResThis.error.message+"; "+expResOther.error.message;
|
|
523
|
+
}
|
|
524
|
+
} else if (expResThis.error) {
|
|
525
|
+
status.reason = "This expansion failed: "+expResThis.error.message
|
|
526
|
+
} else {
|
|
527
|
+
status.reason = "Other expansion failed: "+expResOther.error.message
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let expThis = expResThis.vs;
|
|
532
|
+
let expOther = expResOther.vs;
|
|
464
533
|
if (this.isUnclosed(expThis) || this.isUnclosed(expOther)) {
|
|
465
534
|
status.fail = true;
|
|
535
|
+
if (this.isUnclosed(expThis) && this.isUnclosed(expOther)) {
|
|
536
|
+
status.reason = "Both expansions are unclosed."
|
|
537
|
+
} else if (this.isUnclosed(expThis)) {
|
|
538
|
+
status.reason = "This expansion is unclosed."
|
|
539
|
+
} else {
|
|
540
|
+
status.reason = "Other expansion is unclosed."
|
|
541
|
+
}
|
|
466
542
|
return;
|
|
467
543
|
}
|
|
468
544
|
|
|
545
|
+
if ((!expThis.expansion.contains || expThis.expansion.contains.length == 0) && (!expOther.expansion.contains || expOther.expansion.contains.length == 0)) {
|
|
546
|
+
status.empty = true;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
469
549
|
const matcher = new ArrayMatcher((l, r) =>
|
|
470
550
|
this.matchContains(systems, l, r)
|
|
471
551
|
);
|
|
@@ -482,6 +562,11 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
482
562
|
if (matcher.unmatchedRight.length > 0) {
|
|
483
563
|
status.right = true;
|
|
484
564
|
}
|
|
565
|
+
if (matcher.unmatchedLeft.length > 0 || matcher.unmatchedRight.length > 0) {
|
|
566
|
+
diagnostics.common = matcher.matched;
|
|
567
|
+
}
|
|
568
|
+
diagnostics.missing = matcher.unmatchedLeft;
|
|
569
|
+
diagnostics.extra = matcher.unmatchedRight;
|
|
485
570
|
}
|
|
486
571
|
|
|
487
572
|
isUnclosed(vs) {
|
|
@@ -495,7 +580,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
495
580
|
if (thisC.code != otherC.code) {
|
|
496
581
|
return false;
|
|
497
582
|
}
|
|
498
|
-
let versionMatters = systems.get(thisC.system);
|
|
583
|
+
let versionMatters = systems.has(thisC.system) && systems.get(thisC.system).codes;
|
|
499
584
|
if (versionMatters && thisC.version != otherC.version) {
|
|
500
585
|
return false;
|
|
501
586
|
} else {
|
|
@@ -504,16 +589,25 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
504
589
|
}
|
|
505
590
|
|
|
506
591
|
async doExpand(vs) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
592
|
+
try {
|
|
593
|
+
let txpe = this.params.clone();
|
|
594
|
+
txpe.limit = 10000;
|
|
595
|
+
txpe.excludeNested = true;
|
|
596
|
+
let start = new Date();
|
|
597
|
+
console.log("Expanding value set");
|
|
598
|
+
let exp = new ValueSetExpander(this, txpe);
|
|
599
|
+
exp.noDetails = true;
|
|
600
|
+
let vse = await exp.expand(vs, new SearchFilterText(''), true);
|
|
601
|
+
console.log("Expanded value set - took " + (new Date() - start) + "ms");
|
|
602
|
+
return {vs: vse, error: null};
|
|
603
|
+
} catch (error) {
|
|
604
|
+
debugLog(error, "Error expanding value set");
|
|
605
|
+
return {vs: null, error: error};
|
|
606
|
+
}
|
|
513
607
|
}
|
|
514
608
|
|
|
515
609
|
isConcepts(inc) {
|
|
516
|
-
return inc.concept && inc.concept.length > 0;
|
|
610
|
+
return inc.concept && inc.concept.length > 0 && !this.isFilter(inc);
|
|
517
611
|
}
|
|
518
612
|
|
|
519
613
|
isFilter(inc) {
|
|
@@ -522,52 +616,99 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
522
616
|
|
|
523
617
|
async filterSetsMatch(status, cs, t, o) {
|
|
524
618
|
// two includes have matching filters if the set of filters match.
|
|
525
|
-
|
|
526
|
-
let localstatus = { left: false, right: false};
|
|
527
|
-
|
|
528
|
-
const matcher = new ArrayMatcher((l, r) =>
|
|
529
|
-
this.filtersMatch(localstatus, cs, l, r)
|
|
530
|
-
);
|
|
531
|
-
await matcher.match(t.filter, o.filter);
|
|
532
|
-
|
|
533
|
-
if (matcher.unmatchedLeft.length > 0 || matcher.unmatchedRight.length > 0) {
|
|
619
|
+
if (t.filter.length != o.filter.length) {
|
|
534
620
|
return false;
|
|
621
|
+
}
|
|
622
|
+
if (t.filter.length > 1) {
|
|
623
|
+
t.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value));
|
|
624
|
+
o.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value))
|
|
625
|
+
// we can't draw any conclusions if there's more than one filter, and they aren't identical,
|
|
626
|
+
// because we don't guess how they might interact with each other
|
|
627
|
+
for (let i = 0; i < (t.filter || []).length; i++) {
|
|
628
|
+
if (t.filter[i].property !== o.filter[i].property || t.filter[i].op !== o.filter[i].op || t.filter[i].value !== o.filter[i].value) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
status.common = true;
|
|
633
|
+
return true;
|
|
535
634
|
} else {
|
|
536
|
-
|
|
537
|
-
|
|
635
|
+
let tf = t.filter[0];
|
|
636
|
+
let of = o.filter[0];
|
|
637
|
+
if (tf.property != of.property || tf.op != of.op) {
|
|
638
|
+
return false;
|
|
538
639
|
}
|
|
539
|
-
if (
|
|
540
|
-
status.
|
|
640
|
+
if (tf.value == of.value) {
|
|
641
|
+
status.common = true;
|
|
642
|
+
return true;
|
|
643
|
+
} else if (tf.op == 'is-a') {
|
|
644
|
+
let rel = await cs.subsumesTest(tf.value, of.value)
|
|
645
|
+
switch (rel) {
|
|
646
|
+
case 'equivalent':
|
|
647
|
+
return true;
|
|
648
|
+
case 'subsumes':
|
|
649
|
+
status.common = true;
|
|
650
|
+
status.left = true;
|
|
651
|
+
return true;
|
|
652
|
+
case 'subsumed-by':
|
|
653
|
+
status.common = true;
|
|
654
|
+
status.right = true;
|
|
655
|
+
return true;
|
|
656
|
+
default:
|
|
657
|
+
// we know that the codes aren't related, but we don't know whether they have common children
|
|
658
|
+
// well, that depends on whether there's a multi-heirarchy in play
|
|
659
|
+
if (!cs.hasMultiHierarchy()) {
|
|
660
|
+
status.common = false;
|
|
661
|
+
status.left = true;
|
|
662
|
+
status.right = true;
|
|
663
|
+
return true;
|
|
664
|
+
|
|
665
|
+
} else {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
return false;
|
|
541
671
|
}
|
|
542
|
-
return true;
|
|
543
672
|
}
|
|
544
673
|
}
|
|
545
674
|
|
|
546
|
-
|
|
547
|
-
if (t.
|
|
675
|
+
includesIdentical(t, o) {
|
|
676
|
+
if ((t.concept || []).length !== (o.concept || []).length) {
|
|
548
677
|
return false;
|
|
549
678
|
}
|
|
550
|
-
|
|
551
|
-
|
|
679
|
+
for (let i = 0; i < (t.concept || []).length; i++) {
|
|
680
|
+
if (t.concept[i].code !== o.concept[i].code) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
552
683
|
}
|
|
553
|
-
if (t.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
status.left = true;
|
|
560
|
-
return true;
|
|
561
|
-
case 'subsumed-by':
|
|
562
|
-
status.right = true;
|
|
563
|
-
return true;
|
|
564
|
-
default:
|
|
565
|
-
return false;
|
|
684
|
+
if ((t.filter || []).length !== (o.filter || []).length) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
for (let i = 0; i < (t.filter || []).length; i++) {
|
|
688
|
+
if (t.filter[i].property !== o.filter[i].property || t.filter[i].op !== o.filter[i].op || t.filter[i].value !== o.filter[i].value ) {
|
|
689
|
+
return false;
|
|
566
690
|
}
|
|
567
691
|
}
|
|
568
|
-
|
|
692
|
+
|
|
693
|
+
return true;
|
|
569
694
|
}
|
|
570
695
|
|
|
696
|
+
anyCriteriaHasFilters(allCriteria, key) {
|
|
697
|
+
return allCriteria.some(c => c.system === key && c.filter && c.filter.length > 0);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
hasMultipleVersionsForAnySystem(systems, systemMap) {
|
|
701
|
+
return [...systems.entries()].some(([url, val]) => {
|
|
702
|
+
if (val.criteria !== true) return false;
|
|
703
|
+
let count = 0;
|
|
704
|
+
for (const k of systemMap.keys()) {
|
|
705
|
+
if (k.startsWith(url)) {
|
|
706
|
+
count++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return count > 1;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
571
712
|
}
|
|
572
713
|
|
|
573
714
|
module.exports = {
|
package/tx/workers/search.js
CHANGED
|
@@ -149,57 +149,8 @@ class SearchWorker extends TerminologyWorker {
|
|
|
149
149
|
// If no search params, return all
|
|
150
150
|
const hasSearchParams = Object.keys(searchParams).length > 0;
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (key == cs.vurl) {
|
|
156
|
-
const json = cs.jsonObj;
|
|
157
|
-
|
|
158
|
-
if (!hasSearchParams) {
|
|
159
|
-
matches.push(json);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Check each search parameter for partial match
|
|
164
|
-
let isMatch = true;
|
|
165
|
-
for (const [param, searchValue] of Object.entries(searchParams)) {
|
|
166
|
-
|
|
167
|
-
// Map content-mode to content property
|
|
168
|
-
const jsonProp = param === 'content-mode' ? 'content' : param;
|
|
169
|
-
|
|
170
|
-
if (param === 'jurisdiction') {
|
|
171
|
-
// Special handling for jurisdiction - array of CodeableConcept
|
|
172
|
-
if (!this.matchJurisdiction(json.jurisdiction, searchValue)) {
|
|
173
|
-
isMatch = false;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
} else if (param === 'text') {
|
|
177
|
-
const propValue = json.title + json.description;
|
|
178
|
-
if (!this.matchValue(propValue, searchValue)) {
|
|
179
|
-
isMatch = false;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
} else if (param === 'url' || param === 'system') { // exact match
|
|
183
|
-
const propValue = json.url;
|
|
184
|
-
if (propValue !== searchValue) {
|
|
185
|
-
isMatch = false;
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
// Standard partial text match
|
|
190
|
-
const propValue = json[jsonProp];
|
|
191
|
-
if (!this.matchValue(propValue, searchValue)) {
|
|
192
|
-
isMatch = false;
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (isMatch) {
|
|
199
|
-
matches.push(json);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
152
|
+
this.searchCodeSystemResources(searchParams, hasSearchParams, matches);
|
|
153
|
+
this.searchCodeSystemProviders(searchParams, hasSearchParams, matches);
|
|
203
154
|
|
|
204
155
|
return matches;
|
|
205
156
|
}
|
|
@@ -473,6 +424,130 @@ class SearchWorker extends TerminologyWorker {
|
|
|
473
424
|
|
|
474
425
|
return filtered;
|
|
475
426
|
}
|
|
427
|
+
|
|
428
|
+
searchCodeSystemResources(searchParams, hasSearchParams, matches) {
|
|
429
|
+
for (const [key, cs] of this.provider.codeSystems) {
|
|
430
|
+
this.deadCheck('searchCodeSystems');
|
|
431
|
+
|
|
432
|
+
if (key == cs.vurl) {
|
|
433
|
+
const json = cs.jsonObj;
|
|
434
|
+
|
|
435
|
+
if (!hasSearchParams) {
|
|
436
|
+
matches.push(json);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Check each search parameter for partial match
|
|
441
|
+
let isMatch = true;
|
|
442
|
+
for (const [param, searchValue] of Object.entries(searchParams)) {
|
|
443
|
+
|
|
444
|
+
// Map content-mode to content property
|
|
445
|
+
const jsonProp = param === 'content-mode' ? 'content' : param;
|
|
446
|
+
|
|
447
|
+
if (param === 'jurisdiction') {
|
|
448
|
+
// Special handling for jurisdiction - array of CodeableConcept
|
|
449
|
+
if (!this.matchJurisdiction(json.jurisdiction, searchValue)) {
|
|
450
|
+
isMatch = false;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
} else if (param === 'text') {
|
|
454
|
+
const propValue = json.title + json.description;
|
|
455
|
+
if (!this.matchValue(propValue, searchValue)) {
|
|
456
|
+
isMatch = false;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
} else if (param === 'url' || param === 'system') { // exact match
|
|
460
|
+
const propValue = json.url;
|
|
461
|
+
if (propValue !== searchValue) {
|
|
462
|
+
isMatch = false;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// Standard partial text match
|
|
467
|
+
const propValue = json[jsonProp];
|
|
468
|
+
if (!this.matchValue(propValue, searchValue)) {
|
|
469
|
+
isMatch = false;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (isMatch) {
|
|
476
|
+
matches.push(json);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
searchCodeSystemProviders(searchParams, hasSearchParams, matches) {
|
|
483
|
+
let seen = new Set();
|
|
484
|
+
for (const csp of this.provider.codeSystemFactories.values()) {
|
|
485
|
+
this.deadCheck('searchCodeSystems');
|
|
486
|
+
|
|
487
|
+
if (seen.has(csp.id())) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
seen.add(csp.id());
|
|
491
|
+
|
|
492
|
+
let json = {
|
|
493
|
+
resourceType: "CodeSystem",
|
|
494
|
+
id: "x-" + csp.id(),
|
|
495
|
+
url: csp.system(),
|
|
496
|
+
version: csp.version(),
|
|
497
|
+
name: csp.name(),
|
|
498
|
+
status: "active",
|
|
499
|
+
description: "This is a place holder for the code system which is fully supported through internal means (not by this code system)",
|
|
500
|
+
content: "not-present"
|
|
501
|
+
}
|
|
502
|
+
if (csp.webSource()) {
|
|
503
|
+
json.extension = [{ url: "http://hl7.org/fhir/StructureDefinition/web-source", valueUrl : csp.webSource()}];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!hasSearchParams) {
|
|
507
|
+
matches.push(json);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Check each search parameter for partial match
|
|
512
|
+
let isMatch = true;
|
|
513
|
+
for (const [param, searchValue] of Object.entries(searchParams)) {
|
|
514
|
+
|
|
515
|
+
// Map content-mode to content property
|
|
516
|
+
const jsonProp = param === 'content-mode' ? 'content' : param;
|
|
517
|
+
|
|
518
|
+
if (param === 'jurisdiction') {
|
|
519
|
+
// Special handling for jurisdiction - array of CodeableConcept
|
|
520
|
+
if (!this.matchJurisdiction(json.jurisdiction, searchValue)) {
|
|
521
|
+
isMatch = false;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
} else if (param === 'text') {
|
|
525
|
+
const propValue = json.title + json.description;
|
|
526
|
+
if (!this.matchValue(propValue, searchValue)) {
|
|
527
|
+
isMatch = false;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
} else if (param === 'url' || param === 'system') { // exact match
|
|
531
|
+
const propValue = json.url;
|
|
532
|
+
if (propValue !== searchValue) {
|
|
533
|
+
isMatch = false;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
// Standard partial text match
|
|
538
|
+
const propValue = json[jsonProp];
|
|
539
|
+
if (!this.matchValue(propValue, searchValue)) {
|
|
540
|
+
isMatch = false;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (isMatch) {
|
|
547
|
+
matches.push(json);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
476
551
|
}
|
|
477
552
|
|
|
478
553
|
module.exports = SearchWorker;
|