fhirsmith 0.9.2 → 0.9.3
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 +19 -0
- package/package.json +1 -1
- package/translations/Messages.properties +3 -1
- package/tx/cs/cs-api.js +2 -1
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +1 -1
- package/tx/cs/cs-cpt.js +1 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-hgvs.js +1 -1
- package/tx/cs/cs-lang.js +1 -1
- package/tx/cs/cs-loinc.js +1 -1
- package/tx/cs/cs-ndc.js +1 -1
- package/tx/cs/cs-omop.js +1 -1
- package/tx/cs/cs-rxnorm.js +1 -1
- package/tx/cs/cs-snomed.js +603 -22
- package/tx/cs/cs-ucum.js +1 -1
- package/tx/sct/ecl.js +98 -49
- package/tx/tx.js +6 -2
- package/tx/vs/vs-vsac.js +38 -9
- package/tx/workers/expand.js +42 -24
- package/tx/workers/related.js +1 -1
- package/tx/workers/validate.js +21 -24
- package/tx/workers/worker.js +16 -1
package/tx/cs/cs-ucum.js
CHANGED
|
@@ -256,7 +256,7 @@ class UcumCodeSystemProvider extends BaseCSServices {
|
|
|
256
256
|
// filterContext.filters.push(ucumFilter);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
async filter(filterContext, prop, op, value) {
|
|
259
|
+
async filter(filterContext, forIteration, prop, op, value) {
|
|
260
260
|
assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
|
|
261
261
|
assert(prop != null && typeof prop === 'string', 'prop must be a non-null string');
|
|
262
262
|
assert(op != null && typeof op === 'string', 'op must be a non-null string');
|
package/tx/sct/ecl.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
* Supports ECL v2.1 specification from SNOMED International
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
11
|
-
|
|
12
10
|
// ECL Token Types
|
|
13
11
|
const ECLTokenType = {
|
|
14
12
|
// Literals
|
|
@@ -18,15 +16,15 @@ const ECLTokenType = {
|
|
|
18
16
|
INTEGER: 'INTEGER',
|
|
19
17
|
DECIMAL: 'DECIMAL',
|
|
20
18
|
|
|
21
|
-
// Operators
|
|
22
|
-
CHILD_OF: 'CHILD_OF', //
|
|
23
|
-
CHILD_OR_SELF_OF: 'CHILD_OR_SELF_OF', //
|
|
24
|
-
DESCENDANT_OF: 'DESCENDANT_OF', //
|
|
25
|
-
DESCENDANT_OR_SELF_OF: 'DESCENDANT_OR_SELF_OF', //
|
|
26
|
-
PARENT_OF: 'PARENT_OF', //
|
|
27
|
-
PARENT_OR_SELF_OF: 'PARENT_OR_SELF_OF', //
|
|
28
|
-
ANCESTOR_OF: 'ANCESTOR_OF', //
|
|
29
|
-
ANCESTOR_OR_SELF_OF: 'ANCESTOR_OR_SELF_OF', //
|
|
19
|
+
// Operators (ECL 2.x spec)
|
|
20
|
+
CHILD_OF: 'CHILD_OF', // <! direct children only
|
|
21
|
+
CHILD_OR_SELF_OF: 'CHILD_OR_SELF_OF', // <<! self + direct children
|
|
22
|
+
DESCENDANT_OF: 'DESCENDANT_OF', // < all transitive descendants, no self
|
|
23
|
+
DESCENDANT_OR_SELF_OF: 'DESCENDANT_OR_SELF_OF', // << self + all transitive descendants
|
|
24
|
+
PARENT_OF: 'PARENT_OF', // >! direct parents only
|
|
25
|
+
PARENT_OR_SELF_OF: 'PARENT_OR_SELF_OF', // >>! self + direct parents
|
|
26
|
+
ANCESTOR_OF: 'ANCESTOR_OF', // > all transitive ancestors, no self
|
|
27
|
+
ANCESTOR_OR_SELF_OF: 'ANCESTOR_OR_SELF_OF', // >> self + all transitive ancestors
|
|
30
28
|
|
|
31
29
|
// Set operators
|
|
32
30
|
AND: 'AND',
|
|
@@ -268,29 +266,38 @@ class ECLLexer {
|
|
|
268
266
|
}
|
|
269
267
|
|
|
270
268
|
// Multi-character operators
|
|
269
|
+
// ECL 2.x hierarchy operators:
|
|
270
|
+
// < descendantOf (transitive, no self)
|
|
271
|
+
// << descendantOrSelfOf (transitive, with self)
|
|
272
|
+
// <! childOf (one step)
|
|
273
|
+
// <<! childOrSelfOf (one step + self)
|
|
274
|
+
// > ancestorOf (transitive, no self)
|
|
275
|
+
// >> ancestorOrSelfOf (transitive, with self)
|
|
276
|
+
// >! parentOf (one step)
|
|
277
|
+
// >>! parentOrSelfOf (one step + self)
|
|
271
278
|
if (this.current === '<') {
|
|
272
279
|
if (this.peek() === '<') {
|
|
273
280
|
if (this.peek(2) === '!') {
|
|
274
281
|
this.advance();
|
|
275
282
|
this.advance();
|
|
276
283
|
this.advance();
|
|
277
|
-
return { type: ECLTokenType.
|
|
284
|
+
return { type: ECLTokenType.CHILD_OR_SELF_OF, value: '<<!' };
|
|
278
285
|
} else {
|
|
279
286
|
this.advance();
|
|
280
287
|
this.advance();
|
|
281
|
-
return { type: ECLTokenType.
|
|
288
|
+
return { type: ECLTokenType.DESCENDANT_OR_SELF_OF, value: '<<' };
|
|
282
289
|
}
|
|
283
290
|
} else if (this.peek() === '!') {
|
|
284
291
|
this.advance();
|
|
285
292
|
this.advance();
|
|
286
|
-
return { type: ECLTokenType.
|
|
293
|
+
return { type: ECLTokenType.CHILD_OF, value: '<!' };
|
|
287
294
|
} else if (this.peek() === '=') {
|
|
288
295
|
this.advance();
|
|
289
296
|
this.advance();
|
|
290
297
|
return { type: ECLTokenType.LTE, value: '<=' };
|
|
291
298
|
} else {
|
|
292
299
|
this.advance();
|
|
293
|
-
return { type: ECLTokenType.
|
|
300
|
+
return { type: ECLTokenType.DESCENDANT_OF, value: '<' };
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
|
|
@@ -300,23 +307,23 @@ class ECLLexer {
|
|
|
300
307
|
this.advance();
|
|
301
308
|
this.advance();
|
|
302
309
|
this.advance();
|
|
303
|
-
return { type: ECLTokenType.
|
|
310
|
+
return { type: ECLTokenType.PARENT_OR_SELF_OF, value: '>>!' };
|
|
304
311
|
} else {
|
|
305
312
|
this.advance();
|
|
306
313
|
this.advance();
|
|
307
|
-
return { type: ECLTokenType.
|
|
314
|
+
return { type: ECLTokenType.ANCESTOR_OR_SELF_OF, value: '>>' };
|
|
308
315
|
}
|
|
309
316
|
} else if (this.peek() === '!') {
|
|
310
317
|
this.advance();
|
|
311
318
|
this.advance();
|
|
312
|
-
return { type: ECLTokenType.
|
|
319
|
+
return { type: ECLTokenType.PARENT_OF, value: '>!' };
|
|
313
320
|
} else if (this.peek() === '=') {
|
|
314
321
|
this.advance();
|
|
315
322
|
this.advance();
|
|
316
323
|
return { type: ECLTokenType.GTE, value: '>=' };
|
|
317
324
|
} else {
|
|
318
325
|
this.advance();
|
|
319
|
-
return { type: ECLTokenType.
|
|
326
|
+
return { type: ECLTokenType.ANCESTOR_OF, value: '>' };
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -350,9 +357,9 @@ class ECLLexer {
|
|
|
350
357
|
|
|
351
358
|
// Check if immediately followed by .digit (decimal number)
|
|
352
359
|
if (pos < this.input.length &&
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
this.input[pos] === '.' &&
|
|
361
|
+
pos + 1 < this.input.length &&
|
|
362
|
+
/\d/.test(this.input[pos + 1])) {
|
|
356
363
|
// This is a decimal number - parse it completely
|
|
357
364
|
const num = this.readNumber();
|
|
358
365
|
return { type: num.type, value: num.value };
|
|
@@ -471,8 +478,8 @@ class ECLParser {
|
|
|
471
478
|
const right = this.parseRefinedExpressionConstraint();
|
|
472
479
|
|
|
473
480
|
const nodeType = operator.type === ECLTokenType.AND ? ECLNodeType.CONJUNCTION :
|
|
474
|
-
|
|
475
|
-
|
|
481
|
+
operator.type === ECLTokenType.OR ? ECLNodeType.DISJUNCTION :
|
|
482
|
+
ECLNodeType.EXCLUSION;
|
|
476
483
|
|
|
477
484
|
left = {
|
|
478
485
|
type: ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT,
|
|
@@ -527,10 +534,10 @@ class ECLParser {
|
|
|
527
534
|
// Handle constraint operators
|
|
528
535
|
let operator = null;
|
|
529
536
|
if (this.match(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
537
|
+
ECLTokenType.CHILD_OF, ECLTokenType.CHILD_OR_SELF_OF,
|
|
538
|
+
ECLTokenType.DESCENDANT_OF, ECLTokenType.DESCENDANT_OR_SELF_OF,
|
|
539
|
+
ECLTokenType.PARENT_OF, ECLTokenType.PARENT_OR_SELF_OF,
|
|
540
|
+
ECLTokenType.ANCESTOR_OF, ECLTokenType.ANCESTOR_OR_SELF_OF
|
|
534
541
|
)) {
|
|
535
542
|
operator = this.current;
|
|
536
543
|
this.advance();
|
|
@@ -681,8 +688,11 @@ class ECLParser {
|
|
|
681
688
|
operator: operator.type,
|
|
682
689
|
value
|
|
683
690
|
};
|
|
684
|
-
} else if (this.match(ECLTokenType.LT, ECLTokenType.LTE, ECLTokenType.
|
|
685
|
-
//
|
|
691
|
+
} else if (this.match(ECLTokenType.LT, ECLTokenType.LTE, ECLTokenType.DESCENDANT_OF, ECLTokenType.ANCESTOR_OF, ECLTokenType.GTE)) {
|
|
692
|
+
// In ECL, bare `<` and `>` are overloaded: they lex as hierarchy
|
|
693
|
+
// operators (DESCENDANT_OF / ANCESTOR_OF) but in an attribute comparison
|
|
694
|
+
// context they mean less-than / greater-than. Accept them here and map
|
|
695
|
+
// them to LT / GT so downstream consumers see a uniform shape.
|
|
686
696
|
const operator = this.current;
|
|
687
697
|
this.advance();
|
|
688
698
|
|
|
@@ -697,11 +707,11 @@ class ECLParser {
|
|
|
697
707
|
this.error('Expected numeric value after #');
|
|
698
708
|
}
|
|
699
709
|
|
|
700
|
-
// Map
|
|
710
|
+
// Map DESCENDANT_OF to LT and ANCESTOR_OF to GT for numeric comparisons
|
|
701
711
|
let operatorType = operator.type;
|
|
702
|
-
if (operator.type === ECLTokenType.
|
|
712
|
+
if (operator.type === ECLTokenType.DESCENDANT_OF) {
|
|
703
713
|
operatorType = ECLTokenType.LT;
|
|
704
|
-
} else if (operator.type === ECLTokenType.
|
|
714
|
+
} else if (operator.type === ECLTokenType.ANCESTOR_OF) {
|
|
705
715
|
operatorType = ECLTokenType.GT;
|
|
706
716
|
}
|
|
707
717
|
|
|
@@ -1030,6 +1040,8 @@ class ECLValidator {
|
|
|
1030
1040
|
}
|
|
1031
1041
|
|
|
1032
1042
|
async evaluateWildcard() {
|
|
1043
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1044
|
+
|
|
1033
1045
|
// Return all concepts - this would need optimization in practice
|
|
1034
1046
|
const filter = new SnomedFilterContext();
|
|
1035
1047
|
const allConcepts = [];
|
|
@@ -1046,49 +1058,86 @@ class ECLValidator {
|
|
|
1046
1058
|
}
|
|
1047
1059
|
|
|
1048
1060
|
async evaluateSubExpressionConstraint(node) {
|
|
1061
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1062
|
+
|
|
1049
1063
|
const baseFilter = await this.evaluateAST(node.focus);
|
|
1050
1064
|
|
|
1051
1065
|
if (!node.operator) {
|
|
1052
1066
|
return baseFilter;
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
|
-
// Apply constraint operator
|
|
1056
|
-
|
|
1069
|
+
// Apply constraint operator — collect into a Set to deduplicate across
|
|
1070
|
+
// multi-concept base filters.
|
|
1071
|
+
const accumulated = new Set();
|
|
1057
1072
|
|
|
1058
1073
|
for (const conceptIndex of baseFilter.descendants || []) {
|
|
1059
1074
|
const conceptId = this.sct.concepts.getConceptId(conceptIndex);
|
|
1060
1075
|
|
|
1061
1076
|
let operatorFilter;
|
|
1062
1077
|
switch (node.operator) {
|
|
1063
|
-
|
|
1078
|
+
// ── Descendants ─────────────────────────────────────────────────────
|
|
1079
|
+
case ECLTokenType.DESCENDANT_OF: // < transitive, no self
|
|
1064
1080
|
operatorFilter = this.sct.filterIsA(conceptId, false);
|
|
1065
1081
|
break;
|
|
1066
|
-
case ECLTokenType.
|
|
1082
|
+
case ECLTokenType.DESCENDANT_OR_SELF_OF: // << transitive + self
|
|
1067
1083
|
operatorFilter = this.sct.filterIsA(conceptId, true);
|
|
1068
1084
|
break;
|
|
1069
|
-
case ECLTokenType.
|
|
1070
|
-
operatorFilter = this.sct.
|
|
1085
|
+
case ECLTokenType.CHILD_OF: // <! direct children only
|
|
1086
|
+
operatorFilter = this.sct.filterChildOf(conceptId);
|
|
1071
1087
|
break;
|
|
1072
|
-
case ECLTokenType.
|
|
1073
|
-
operatorFilter =
|
|
1088
|
+
case ECLTokenType.CHILD_OR_SELF_OF: { // <<! self + direct children
|
|
1089
|
+
operatorFilter = new SnomedFilterContext();
|
|
1090
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1091
|
+
const children = selfResult.found ? this.sct.getConceptChildren(selfResult.index) : [];
|
|
1092
|
+
operatorFilter.descendants = selfResult.found ? [selfResult.index, ...children] : children;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ── Ancestors ───────────────────────────────────────────────────────
|
|
1097
|
+
case ECLTokenType.ANCESTOR_OF: // > transitive, no self
|
|
1098
|
+
operatorFilter = this.sct.filterGeneralizes(conceptId);
|
|
1099
|
+
break;
|
|
1100
|
+
case ECLTokenType.ANCESTOR_OR_SELF_OF: { // >> transitive + self
|
|
1101
|
+
operatorFilter = this.sct.filterGeneralizes(conceptId);
|
|
1102
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1103
|
+
if (selfResult.found && !operatorFilter.descendants.includes(selfResult.index)) {
|
|
1104
|
+
operatorFilter.descendants.push(selfResult.index);
|
|
1105
|
+
}
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case ECLTokenType.PARENT_OF: { // >! direct parents only
|
|
1109
|
+
operatorFilter = new SnomedFilterContext();
|
|
1110
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1111
|
+
operatorFilter.descendants = selfResult.found
|
|
1112
|
+
? this.sct.getConceptParents(selfResult.index)
|
|
1113
|
+
: [];
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case ECLTokenType.PARENT_OR_SELF_OF: { // >>! self + direct parents
|
|
1117
|
+
operatorFilter = new SnomedFilterContext();
|
|
1118
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1119
|
+
const parents = selfResult.found ? this.sct.getConceptParents(selfResult.index) : [];
|
|
1120
|
+
operatorFilter.descendants = selfResult.found ? [selfResult.index, ...parents] : parents;
|
|
1074
1121
|
break;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
case ECLTokenType.ANCESTOR_OF:
|
|
1078
|
-
case ECLTokenType.ANCESTOR_OR_SELF_OF:
|
|
1079
|
-
// These would require reverse hierarchy traversal
|
|
1080
|
-
throw new Error(`Operator ${node.operator} not yet implemented`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1081
1124
|
default:
|
|
1082
1125
|
throw new Error(`Unknown constraint operator: ${node.operator}`);
|
|
1083
1126
|
}
|
|
1084
1127
|
|
|
1085
|
-
|
|
1128
|
+
for (const idx of operatorFilter.descendants || []) {
|
|
1129
|
+
accumulated.add(idx);
|
|
1130
|
+
}
|
|
1086
1131
|
}
|
|
1087
1132
|
|
|
1133
|
+
const results = new SnomedFilterContext();
|
|
1134
|
+
results.descendants = [...accumulated];
|
|
1088
1135
|
return results;
|
|
1089
1136
|
}
|
|
1090
1137
|
|
|
1091
1138
|
async evaluateCompoundExpression(node) {
|
|
1139
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1140
|
+
|
|
1092
1141
|
const leftFilter = await this.evaluateAST(node.left);
|
|
1093
1142
|
const rightFilter = await this.evaluateAST(node.right);
|
|
1094
1143
|
|
|
@@ -1327,7 +1376,7 @@ class ECLValidator {
|
|
|
1327
1376
|
this.validateSemanticAST(node.value, errors);
|
|
1328
1377
|
break;
|
|
1329
1378
|
|
|
1330
|
-
|
|
1379
|
+
// Basic nodes don't need semantic validation
|
|
1331
1380
|
case ECLNodeType.CONCEPT_REFERENCE:
|
|
1332
1381
|
case ECLNodeType.WILDCARD:
|
|
1333
1382
|
break;
|
package/tx/tx.js
CHANGED
|
@@ -20,7 +20,7 @@ const packageJson = require("../package.json");
|
|
|
20
20
|
// Import workers
|
|
21
21
|
const ReadWorker = require('./workers/read');
|
|
22
22
|
const SearchWorker = require('./workers/search');
|
|
23
|
-
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT,
|
|
23
|
+
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_TEST_DEFAULT_LIMIT} = require('./workers/expand');
|
|
24
24
|
const { ValidateWorker } = require('./workers/validate');
|
|
25
25
|
const TranslateWorker = require('./workers/translate');
|
|
26
26
|
const LookupWorker = require('./workers/lookup');
|
|
@@ -1212,8 +1212,12 @@ class TXModule {
|
|
|
1212
1212
|
}
|
|
1213
1213
|
|
|
1214
1214
|
externalLimit(req) {
|
|
1215
|
+
let hdr = req.headers["x-too-costly-threshold"];
|
|
1216
|
+
if (hdr) {
|
|
1217
|
+
return parseInt(hdr);
|
|
1218
|
+
}
|
|
1215
1219
|
let isTest = req.header("User-Agent") == 'Tools/Java';
|
|
1216
|
-
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return
|
|
1220
|
+
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_TEST_DEFAULT_LIMIT;
|
|
1217
1221
|
}
|
|
1218
1222
|
|
|
1219
1223
|
}
|
package/tx/vs/vs-vsac.js
CHANGED
|
@@ -136,7 +136,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
136
136
|
console.log('Starting VSAC ValueSet refresh...');
|
|
137
137
|
|
|
138
138
|
// This lists all the currently valid value sets by URL, but not the older versions
|
|
139
|
-
let url = '/ValueSet?_offset=0&_count=
|
|
139
|
+
let url = '/ValueSet?_offset=0&_count=1000&_elements=id,url,version,status';
|
|
140
140
|
|
|
141
141
|
let total = undefined;
|
|
142
142
|
let count = 0;
|
|
@@ -669,7 +669,23 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
669
669
|
);
|
|
670
670
|
});
|
|
671
671
|
|
|
672
|
-
|
|
672
|
+
// ISO date (YYYY-MM-DD UTC) for grouping
|
|
673
|
+
const dayKey = ts => ts
|
|
674
|
+
? new Date(ts * 1000).toISOString().substring(0, 10)
|
|
675
|
+
: '';
|
|
676
|
+
// Human-friendly day heading e.g. "Tuesday, 14 April 2026"
|
|
677
|
+
const dayLabel = ts => ts
|
|
678
|
+
? new Date(ts * 1000).toLocaleDateString('en-GB', {
|
|
679
|
+
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
|
680
|
+
timeZone: 'UTC'
|
|
681
|
+
})
|
|
682
|
+
: '—';
|
|
683
|
+
// HH:MM:SS UTC within a day
|
|
684
|
+
const timeOnly = ts => ts
|
|
685
|
+
? new Date(ts * 1000).toISOString().substring(11, 19) + ' UTC'
|
|
686
|
+
: '—';
|
|
687
|
+
// Full timestamp (used in "Running..." detail where context is needed)
|
|
688
|
+
const fmtFull = ts => ts
|
|
673
689
|
? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
|
|
674
690
|
: '—';
|
|
675
691
|
|
|
@@ -678,7 +694,16 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
678
694
|
html += '<thead><tr><th>Time</th><th>Event</th><th>Detail</th></tr></thead>';
|
|
679
695
|
html += '<tbody>';
|
|
680
696
|
|
|
697
|
+
let currentDay = null;
|
|
681
698
|
for (const row of rows) {
|
|
699
|
+
const rowDay = dayKey(row.ts);
|
|
700
|
+
if (rowDay !== currentDay) {
|
|
701
|
+
currentDay = rowDay;
|
|
702
|
+
html += `<tr style="background:#d8d8d8">`;
|
|
703
|
+
html += `<td colspan="3"><strong>${escape(dayLabel(row.ts))}</strong></td>`;
|
|
704
|
+
html += `</tr>`;
|
|
705
|
+
}
|
|
706
|
+
|
|
682
707
|
if (row.kind === 'run') {
|
|
683
708
|
const duration = row.finished_at ? `${row.finished_at - row.ts}s` : 'in progress';
|
|
684
709
|
let detail, colour;
|
|
@@ -690,11 +715,11 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
690
715
|
detail = `Failed: ${escape(row.error_message || '')} (${duration})`;
|
|
691
716
|
colour = 'red';
|
|
692
717
|
} else {
|
|
693
|
-
detail = `Running... (started ${
|
|
718
|
+
detail = `Running... (started ${fmtFull(row.ts)})`;
|
|
694
719
|
colour = 'orange';
|
|
695
720
|
}
|
|
696
721
|
html += `<tr style="background:#f0f0f0">`;
|
|
697
|
-
html += `<td>${escape(
|
|
722
|
+
html += `<td>${escape(timeOnly(row.ts))}</td>`;
|
|
698
723
|
html += `<td><strong style="color:${colour}">Sync run</strong></td>`;
|
|
699
724
|
html += `<td>${detail}</td>`;
|
|
700
725
|
html += `</tr>`;
|
|
@@ -703,15 +728,15 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
703
728
|
let label, colour;
|
|
704
729
|
switch (row.event_type) {
|
|
705
730
|
case 'new':
|
|
706
|
-
label = 'New
|
|
731
|
+
label = 'New';
|
|
707
732
|
colour = 'green';
|
|
708
733
|
break;
|
|
709
734
|
case 'updated':
|
|
710
|
-
label = 'Updated
|
|
735
|
+
label = 'Updated';
|
|
711
736
|
colour = 'blue';
|
|
712
737
|
break;
|
|
713
738
|
case 'deleted':
|
|
714
|
-
label = 'Deleted
|
|
739
|
+
label = 'Deleted';
|
|
715
740
|
colour = 'red';
|
|
716
741
|
break;
|
|
717
742
|
default:
|
|
@@ -719,9 +744,9 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
719
744
|
colour = 'black';
|
|
720
745
|
}
|
|
721
746
|
html += `<tr>`;
|
|
722
|
-
html += `<td>${escape(
|
|
747
|
+
html += `<td>${escape(timeOnly(row.ts))}</td>`;
|
|
723
748
|
html += `<td><span style="color:${colour}">${label}</span></td>`;
|
|
724
|
-
html += `<td>${escape(row.url || '')}
|
|
749
|
+
html += `<td>${escape(this.urlTail(row.url) || '')} v <a href="../ValueSet/${escape(this.urlTail(row.url) || '')}-${escape(row.version || '')}">${escape(row.version || '')}</a></td>`;
|
|
725
750
|
html += `</tr>`;
|
|
726
751
|
}
|
|
727
752
|
}
|
|
@@ -733,6 +758,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
733
758
|
id() {
|
|
734
759
|
return "vsac";
|
|
735
760
|
}
|
|
761
|
+
|
|
762
|
+
urlTail(url) {
|
|
763
|
+
return url ? url.substring(url.lastIndexOf('/') + 1) : '';
|
|
764
|
+
}
|
|
736
765
|
}
|
|
737
766
|
|
|
738
767
|
// Usage examples:
|
package/tx/workers/expand.js
CHANGED
|
@@ -22,6 +22,7 @@ const {debugLog} = require("../operation-context");
|
|
|
22
22
|
|
|
23
23
|
// Expansion limits (from Pascal constants)
|
|
24
24
|
const EXTERNAL_DEFAULT_LIMIT = 1000;
|
|
25
|
+
const EXTERNAL_TEST_DEFAULT_LIMIT = 3000;
|
|
25
26
|
const INTERNAL_DEFAULT_LIMIT = 10000;
|
|
26
27
|
const EXPANSION_DEAD_TIME_SECS = 30;
|
|
27
28
|
const CACHE_WHEN_DEBUGGING = false;
|
|
@@ -508,8 +509,8 @@ class ValueSetExpander {
|
|
|
508
509
|
this.excluded.add(key);
|
|
509
510
|
}
|
|
510
511
|
|
|
511
|
-
async checkCanExpandValueSet(uri, version) {
|
|
512
|
-
const vs = await this.worker.findValueSet(uri, version);
|
|
512
|
+
async checkCanExpandValueSet(uri, version, source) {
|
|
513
|
+
const vs = await this.worker.findValueSet(uri, version, source);
|
|
513
514
|
if (vs == null) {
|
|
514
515
|
if (!version && uri.includes('|')) {
|
|
515
516
|
version = uri.substring(uri.indexOf('|') + 1);
|
|
@@ -525,9 +526,8 @@ class ValueSetExpander {
|
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
|
|
528
|
-
async expandValueSet(uri, version, filter, notClosed) {
|
|
529
|
+
async expandValueSet(uri, version, vs, filter, notClosed) {
|
|
529
530
|
|
|
530
|
-
let vs = await this.worker.findValueSet(uri, version);
|
|
531
531
|
if (!vs) {
|
|
532
532
|
if (version) {
|
|
533
533
|
throw new Issue('error', 'not-found', null, 'VS_EXP_IMPORT_UNK_PINNED', this.worker.i18n.translate('VS_EXP_IMPORT_UNK_PINNED', this.params.httpLanguages, [uri, version]), "not-found", 422);
|
|
@@ -609,14 +609,14 @@ class ValueSetExpander {
|
|
|
609
609
|
}
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
async checkSource(cset, exp, filter, srcURL, ts, vsInfo) {
|
|
612
|
+
async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
|
|
613
613
|
this.worker.deadCheck('checkSource');
|
|
614
614
|
Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set', srcURL);
|
|
615
615
|
let imp = false;
|
|
616
616
|
for (const u of cset.valueSet || []) {
|
|
617
617
|
this.worker.deadCheck('checkSource');
|
|
618
618
|
const s = this.worker.pinValueSet(u);
|
|
619
|
-
await this.checkCanExpandValueSet(s, '');
|
|
619
|
+
await this.checkCanExpandValueSet(s, '', source);
|
|
620
620
|
imp = true;
|
|
621
621
|
}
|
|
622
622
|
|
|
@@ -659,7 +659,7 @@ class ValueSetExpander {
|
|
|
659
659
|
|
|
660
660
|
if (!cset.concept && !cset.filter) {
|
|
661
661
|
if (cs.specialEnumeration()) {
|
|
662
|
-
await this.checkCanExpandValueSet(cs.specialEnumeration(), '');
|
|
662
|
+
await this.checkCanExpandValueSet(cs.specialEnumeration(), '', null);
|
|
663
663
|
} else if (filter.isNull) {
|
|
664
664
|
if (cs.isNotClosed()) {
|
|
665
665
|
if (cs.specialEnumeration()) {
|
|
@@ -704,9 +704,12 @@ class ValueSetExpander {
|
|
|
704
704
|
this.worker.deadCheck('processCodes#2');
|
|
705
705
|
const s = this.worker.pinValueSet(u);
|
|
706
706
|
this.worker.opContext.log('import value set ' + s);
|
|
707
|
-
|
|
708
|
-
this.
|
|
709
|
-
this.
|
|
707
|
+
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
708
|
+
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
709
|
+
this. checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
710
|
+
if (!vs.isContained) {
|
|
711
|
+
this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
|
|
712
|
+
}
|
|
710
713
|
valueSets.push(ivs);
|
|
711
714
|
}
|
|
712
715
|
this.addToTotal(await this.importValueSet(valueSets[0].valueSet, expansion, valueSets, 1));
|
|
@@ -728,16 +731,20 @@ class ValueSetExpander {
|
|
|
728
731
|
this.worker.deadCheck('processCodes#2');
|
|
729
732
|
const s = this.worker.pinValueSet(u);
|
|
730
733
|
this.worker.opContext.log('import value set ' + s);
|
|
731
|
-
|
|
734
|
+
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
735
|
+
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
732
736
|
this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
733
|
-
|
|
737
|
+
if (!vs.isContained) {
|
|
738
|
+
this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
|
|
739
|
+
}
|
|
734
740
|
valueSets.push(ivs);
|
|
735
741
|
}
|
|
736
742
|
|
|
737
743
|
if (!cset.concept && !cset.filter) {
|
|
738
744
|
if (cs.specialEnumeration() && filters.length === 0) {
|
|
739
745
|
this.worker.opContext.log('import special value set ' + cs.specialEnumeration());
|
|
740
|
-
|
|
746
|
+
let vs = await this.worker.findValueSet(cs.specialEnumeration(), '', null);
|
|
747
|
+
const base = await this.expandValueSet(cs.specialEnumeration(), '', vs, filter, notClosed);
|
|
741
748
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
742
749
|
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
743
750
|
await this.importValueSet(base, expansion, valueSets, 0);
|
|
@@ -860,7 +867,7 @@ class ValueSetExpander {
|
|
|
860
867
|
throw new Issue('error', 'invalid', path + ".filter[" + i + "]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.httpLanguages, [cs.system(), fc.property, fc.op]), 'vs-invalid', 400);
|
|
861
868
|
}
|
|
862
869
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
863
|
-
await cs.filter(prep, fc.property, fc.op, fc.value);
|
|
870
|
+
await cs.filter(prep, i == 0, fc.property, fc.op, fc.value);
|
|
864
871
|
}
|
|
865
872
|
|
|
866
873
|
const fset = await cs.executeFilters(prep);
|
|
@@ -871,6 +878,7 @@ class ValueSetExpander {
|
|
|
871
878
|
}
|
|
872
879
|
|
|
873
880
|
this.worker.opContext.log('iterate filters');
|
|
881
|
+
this.addToTotal(0);
|
|
874
882
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
875
883
|
while (await cs.filterMore(prep, fset[0])) {
|
|
876
884
|
this.worker.deadCheck('processCodes#5');
|
|
@@ -937,9 +945,12 @@ class ValueSetExpander {
|
|
|
937
945
|
for (const u of cset.valueSet) {
|
|
938
946
|
const s = this.worker.pinValueSet(u);
|
|
939
947
|
this.worker.deadCheck('processCodes#2');
|
|
940
|
-
|
|
948
|
+
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
949
|
+
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
941
950
|
this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
942
|
-
|
|
951
|
+
if (!vs.isContained) {
|
|
952
|
+
this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl);
|
|
953
|
+
}
|
|
943
954
|
valueSets.push(ivs);
|
|
944
955
|
}
|
|
945
956
|
this.excludeValueSet(valueSets[0].valueSet, expansion, valueSets, 1);
|
|
@@ -959,9 +970,12 @@ class ValueSetExpander {
|
|
|
959
970
|
this.worker.deadCheck('processCodes#3');
|
|
960
971
|
const s = this.worker.pinValueSet(u);
|
|
961
972
|
this.worker.opContext.log('import value set ' + s);
|
|
962
|
-
|
|
973
|
+
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
974
|
+
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
963
975
|
this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
964
|
-
|
|
976
|
+
if (!vs.isContained) {
|
|
977
|
+
this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
|
|
978
|
+
}
|
|
965
979
|
valueSets.push(ivs);
|
|
966
980
|
}
|
|
967
981
|
|
|
@@ -1039,10 +1053,12 @@ class ValueSetExpander {
|
|
|
1039
1053
|
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
1040
1054
|
}
|
|
1041
1055
|
|
|
1056
|
+
let first = true;
|
|
1042
1057
|
for (let fc of cset.filter) {
|
|
1043
1058
|
this.worker.deadCheck('processCodes#4a');
|
|
1044
1059
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
1045
|
-
await cs.filter(prep, fc.property, fc.op, fc.value);
|
|
1060
|
+
await cs.filter(prep, first, fc.property, fc.op, fc.value);
|
|
1061
|
+
first = false;
|
|
1046
1062
|
}
|
|
1047
1063
|
|
|
1048
1064
|
this.worker.opContext.log('iterate filters');
|
|
@@ -1150,12 +1166,12 @@ class ValueSetExpander {
|
|
|
1150
1166
|
const ts = new Map();
|
|
1151
1167
|
for (const c of source.jsonObj.compose.include || []) {
|
|
1152
1168
|
this.worker.deadCheck('handleCompose#2');
|
|
1153
|
-
await this.checkSource(c, expansion, filter, source.url, ts, vsInfo);
|
|
1169
|
+
await this.checkSource(c, expansion, filter, source.url, ts, vsInfo, source);
|
|
1154
1170
|
}
|
|
1155
1171
|
for (const c of source.jsonObj.compose.exclude || []) {
|
|
1156
1172
|
this.worker.deadCheck('handleCompose#3');
|
|
1157
1173
|
this.hasExclusions = true;
|
|
1158
|
-
await this.checkSource(c, expansion, filter, source.url, ts, null);
|
|
1174
|
+
await this.checkSource(c, expansion, filter, source.url, ts, null, source);
|
|
1159
1175
|
}
|
|
1160
1176
|
|
|
1161
1177
|
this.worker.opContext.log('compose #2');
|
|
@@ -1215,6 +1231,7 @@ class ValueSetExpander {
|
|
|
1215
1231
|
result.publisher = undefined;
|
|
1216
1232
|
result.extension = undefined;
|
|
1217
1233
|
result.text = undefined;
|
|
1234
|
+
result.contained = undefined;
|
|
1218
1235
|
}
|
|
1219
1236
|
|
|
1220
1237
|
for (let s of this.params.supplements) this.requiredSupplements.add(s);
|
|
@@ -1909,7 +1926,7 @@ class ExpandWorker extends TerminologyWorker {
|
|
|
1909
1926
|
const url = this.getParameterValue(urlParam);
|
|
1910
1927
|
const version = versionParam ? this.getParameterValue(versionParam) : null;
|
|
1911
1928
|
|
|
1912
|
-
valueSet = await this.findValueSet(url, version);
|
|
1929
|
+
valueSet = await this.findValueSet(url, version, null);
|
|
1913
1930
|
this.seeSourceVS(valueSet, url);
|
|
1914
1931
|
if (!valueSet) {
|
|
1915
1932
|
return res.status(422).json(this.operationOutcome('error', 'not-found',
|
|
@@ -2072,8 +2089,8 @@ class ExpandWorker extends TerminologyWorker {
|
|
|
2072
2089
|
|
|
2073
2090
|
if (params.limit < -1) {
|
|
2074
2091
|
params.limit = -1;
|
|
2075
|
-
} else if (params.limit >
|
|
2076
|
-
params.limit =
|
|
2092
|
+
} else if (params.limit > this.externalLimit) {
|
|
2093
|
+
params.limit = this.externalLimit; // can't ask for more than this externally, though you can internally
|
|
2077
2094
|
}
|
|
2078
2095
|
|
|
2079
2096
|
const filter = new SearchFilterText(params.filter);
|
|
@@ -2123,6 +2140,7 @@ module.exports = {
|
|
|
2123
2140
|
EmptyFilterContext,
|
|
2124
2141
|
EXTERNAL_DEFAULT_LIMIT,
|
|
2125
2142
|
INTERNAL_DEFAULT_LIMIT,
|
|
2143
|
+
EXTERNAL_TEST_DEFAULT_LIMIT,
|
|
2126
2144
|
TotalStatus,
|
|
2127
2145
|
EXPANSION_DEAD_TIME_SECS
|
|
2128
2146
|
};
|
package/tx/workers/related.js
CHANGED
|
@@ -188,7 +188,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
188
188
|
const url = this.getParameterValue(urlParam);
|
|
189
189
|
const version = versionParam ? this.getParameterValue(versionParam) : null;
|
|
190
190
|
|
|
191
|
-
let valueSet = await this.findValueSet(url, version);
|
|
191
|
+
let valueSet = await this.findValueSet(url, version, null);
|
|
192
192
|
this.seeSourceVS(valueSet, url);
|
|
193
193
|
if (!valueSet) {
|
|
194
194
|
return res.status(404).json(this.operationOutcome('error', 'not-found',
|