From 3d1a8a26756a55091cc34f1f0babab828bf6da5c Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 07:35:52 +1000 Subject: [PATCH 1/6] add missing deadChecks --- tx/cs/cs-cs.js | 8 ++++ tx/cs/cs-loinc.js | 1 + tx/cs/cs-snomed.js | 99 ++++++++++++++++++++++++++-------------------- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/tx/cs/cs-cs.js b/tx/cs/cs-cs.js index d531eeb9..9db264c0 100644 --- a/tx/cs/cs-cs.js +++ b/tx/cs/cs-cs.js @@ -1140,6 +1140,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const allConcepts = this.codeSystem.getAllConcepts(); for (const concept of allConcepts) { + this.opContext.deadCheck('cs:searchFilter'); const rating = this._calculateSearchRating(concept, searchTerm); if (rating > 0) { results.add(concept, rating); @@ -1285,6 +1286,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const allCodes = this.codeSystem.getAllCodes(); for (const code of allCodes) { + this.opContext.deadCheck('cs:conceptFilter:is-not-a'); if (!excludeSet.has(code)) { const concept = this.codeSystem.getConceptByCode(code); if (concept) { @@ -1316,6 +1318,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const regex = regexUtilities.compile('^' + value + '$'); const allCodes = this.codeSystem.getAllCodes(); for (const code of allCodes) { + this.opContext.deadCheck('cs:conceptFilter:regex'); if (regex.test(code)) { const concept = this.codeSystem.getConceptByCode(code); if (concept) { @@ -1349,6 +1352,7 @@ class FhirCodeSystemProvider extends BaseCSServices { } const descendants = this.codeSystem.getDescendants(ancestorCode); for (const code of descendants) { + this.opContext.deadCheck('cs:addDescendants'); if (code !== ancestorCode) { const concept = this.codeSystem.getConceptByCode(code); if (concept) { @@ -1370,6 +1374,7 @@ class FhirCodeSystemProvider extends BaseCSServices { if (concept) { const descendants = this.codeSystem.getChildren(parentCode); for (const code of descendants) { + this.opContext.deadCheck('cs:addChildren'); if (code !== parentCode) { // should not be const concept = this.codeSystem.getConceptByCode(code); if (concept) { @@ -1393,6 +1398,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const allCodes = this.codeSystem.getAllCodes(); for (const code of allCodes) { + this.opContext.deadCheck('cs:childExistsFilter'); const hasChildren = this.codeSystem.getChildren(code).length > 0; if (hasChildren === wantChildren) { const concept = this.codeSystem.getConceptByCode(code); @@ -1419,6 +1425,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const allConcepts = this.codeSystem.getAllConcepts(); for (const concept of allConcepts) { + this.opContext.deadCheck('cs:propertyFilter'); if (this._conceptMatchesPropertyFilter(concept, propertyDef, op, value)) { results.add(concept, 0); } @@ -1498,6 +1505,7 @@ class FhirCodeSystemProvider extends BaseCSServices { const allConcepts = this.codeSystem.getAllConcepts(); for (const concept of allConcepts) { + this.opContext.deadCheck('cs:knownPropertyFilter'); let matches = false; if (prop === 'notSelectable') { diff --git a/tx/cs/cs-loinc.js b/tx/cs/cs-loinc.js index d6615d2c..01f4ff38 100644 --- a/tx/cs/cs-loinc.js +++ b/tx/cs/cs-loinc.js @@ -947,6 +947,7 @@ class LoincServices extends BaseCSServices { reject(err); } else { for (const row of rows) { + if (this.opContext) this.opContext.deadCheck('loinc:findRegexMatches'); if (regex.test(row[valueColumn])) { matchingKeys.push(row[keyColumn]); } diff --git a/tx/cs/cs-snomed.js b/tx/cs/cs-snomed.js index f00107ec..2065f73f 100644 --- a/tx/cs/cs-snomed.js +++ b/tx/cs/cs-snomed.js @@ -374,7 +374,7 @@ class SnomedServices { } - filterGeneralizes(id = true) { + filterGeneralizes(id = true, opContext = null) { const result = new SnomedFilterContext(); const conceptResult = this.concepts.findConcept(id); @@ -386,6 +386,7 @@ class SnomedServices { let parents = this.getConceptParents(conceptResult.index); let isNew = true; while (isNew) { + if (opContext) opContext.deadCheck('ecl:filterGeneralizes'); isNew = false; let np = []; for (let parent of parents) { @@ -485,7 +486,7 @@ class SnomedServices { } let result; try { - result = this._evalECLNode(ast); + result = this._evalECLNode(ast, opContext); } catch (err) { debugLog(err); throw new Issue('error', 'invalid', null, 'UNSUPPORTED_ECL', opContext.i18n.translate('UNSUPPORTED_ECL', opContext.langs, [eclExpression, err.message]), 'vs-invalid').handleAsOO(400); @@ -495,7 +496,7 @@ class SnomedServices { // we actually need the full concept list, otherwise filterSize returns 0 // and the iteration yields nothing. Materialise active concepts now. if (forIteration && result.eclWildcard && (!result.descendants || result.descendants.length === 0)) { - result.descendants = this._eclEnumerateActiveConcepts(); + result.descendants = this._eclEnumerateActiveConcepts(opContext); delete result.eclWildcard; } return result; @@ -506,10 +507,11 @@ class SnomedServices { * when the filter needs to be iterated over (e.g. $expand). * @returns {number[]} */ - _eclEnumerateActiveConcepts = function () { + _eclEnumerateActiveConcepts = function (opContext) { const all = []; const n = this.concepts.count(); for (let i = 0; i < n; i++) { + if (opContext) opContext.deadCheck('ecl:enumerateActiveConcepts'); const concept = this.concepts.getConceptByCount(i); if ((concept.flags & 0x0F) === 0) { // active all.push(concept.index); @@ -523,36 +525,37 @@ class SnomedServices { * @param {object} node * @returns {SnomedFilterContext} */ - _evalECLNode = function (node) { + _evalECLNode = function (node, opContext) { if (!node) { throw new Error('ECL evaluation error: null AST node'); } + if (opContext) opContext.deadCheck('ecl:evalNode'); switch (node.type) { case ECLNodeType.SUB_EXPRESSION_CONSTRAINT: - return this._evalSubExpression(node); + return this._evalSubExpression(node, opContext); case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT: { - const left = this._evalECLNode(node.left); - const right = this._evalECLNode(node.right); + const left = this._evalECLNode(node.left, opContext); + const right = this._evalECLNode(node.right, opContext); switch (node.operator) { case ECLNodeType.CONJUNCTION: - return this._eclIntersect(left, right); + return this._eclIntersect(left, right, opContext); case ECLNodeType.DISJUNCTION: - return this._eclUnion(left, right); + return this._eclUnion(left, right, opContext); case ECLNodeType.EXCLUSION: - return this._eclMinus(left, right); + return this._eclMinus(left, right, opContext); default: throw new Error(`Unsupported ECL compound operator: ${node.operator}`); } } case ECLNodeType.REFINED_EXPRESSION_CONSTRAINT: - return this._evalRefined(node); + return this._evalRefined(node, opContext); case ECLNodeType.DOTTED_EXPRESSION_CONSTRAINT: - return this._evalDotted(node); + return this._evalDotted(node, opContext); default: // Could be a bare concept reference or wildcard passed in directly @@ -561,7 +564,7 @@ class SnomedServices { node.type === ECLNodeType.WILDCARD || node.type === ECLNodeType.MEMBER_OF) { // Wrap it as if it came from a no-operator SubExpressionConstraint - return this._evalSubExpression({type: ECLNodeType.SUB_EXPRESSION_CONSTRAINT, operator: null, focus: node}); + return this._evalSubExpression({type: ECLNodeType.SUB_EXPRESSION_CONSTRAINT, operator: null, focus: node}, opContext); } throw new Error(`Unsupported ECL node type: ${node.type}`); } @@ -573,7 +576,7 @@ class SnomedServices { * @param {object} node * @returns {SnomedFilterContext} */ - _evalSubExpression = function (node) { + _evalSubExpression = function (node, opContext) { const operator = node.operator; // an ECLTokenType string, or null const focus = node.focus; @@ -595,11 +598,11 @@ class SnomedServices { // Plain concept reference if (focus.type === ECLNodeType.CONCEPT_REFERENCE) { - return this._evalConceptWithOperator(focus.conceptId, operator); + return this._evalConceptWithOperator(focus.conceptId, operator, opContext); } // Parenthesised sub-expression: focus is itself a full constraint node - return this._evalECLNode(focus); + return this._evalECLNode(focus, opContext); }; /** @@ -608,7 +611,7 @@ class SnomedServices { * @param {string|null} operator ECLTokenType constant * @returns {SnomedFilterContext} */ - _evalConceptWithOperator = function (conceptId, operator) { + _evalConceptWithOperator = function (conceptId, operator, opContext) { switch (operator) { case null: case undefined: @@ -640,7 +643,7 @@ class SnomedServices { // ── Ancestors ────────────────────────────────────────────────────────── case ECLTokenType.ANCESTOR_OR_SELF_OF: { // >> self + all transitive ancestors - const result = this.filterGeneralizes(conceptId); + const result = this.filterGeneralizes(conceptId, opContext); const self = this.concepts.findConcept(conceptId); if (self.found && !result.descendants.includes(self.index)) { result.descendants.push(self.index); @@ -649,7 +652,7 @@ class SnomedServices { } case ECLTokenType.ANCESTOR_OF: { // > all transitive ancestors, no self - return this.filterGeneralizes(conceptId); + return this.filterGeneralizes(conceptId, opContext); } case ECLTokenType.PARENT_OR_SELF_OF: { // >>! self + direct parents only @@ -714,8 +717,8 @@ class SnomedServices { * @param {object} node * @returns {SnomedFilterContext} */ - _evalDotted = function (node) { - let current = this._eclResolveSet(this._evalECLNode(node.base)); + _evalDotted = function (node, opContext) { + let current = this._eclResolveSet(this._evalECLNode(node.base, opContext), opContext); for (const attr of node.attributes || []) { if (attr.type !== ECLNodeType.CONCEPT_REFERENCE) { @@ -729,6 +732,7 @@ class SnomedServices { const next = new Set(); for (const conceptIdx of current) { + if (opContext) opContext.deadCheck('ecl:dotted'); const relIdxs = this.getConceptRelationships(conceptIdx); for (const relIdx of relIdxs) { const rel = this.relationships.getRelationship(relIdx); @@ -758,11 +762,12 @@ class SnomedServices { * @param {object} node * @returns {SnomedFilterContext} */ - _evalRefined = function (node) { - const baseSet = this._eclResolveSet(this._evalECLNode(node.base)); + _evalRefined = function (node, opContext) { + const baseSet = this._eclResolveSet(this._evalECLNode(node.base, opContext), opContext); const matching = []; for (const conceptIdx of baseSet) { - if (this._refinementMatches(conceptIdx, node.refinement)) { + if (opContext) opContext.deadCheck('ecl:refined'); + if (this._refinementMatches(conceptIdx, node.refinement, opContext)) { matching.push(conceptIdx); } } @@ -778,17 +783,17 @@ class SnomedServices { * @param {object} refinement * @returns {boolean} */ - _refinementMatches = function (conceptIdx, refinement) { + _refinementMatches = function (conceptIdx, refinement, opContext) { switch (refinement.type) { case ECLNodeType.ATTRIBUTE: - return this._attributeMatches(conceptIdx, refinement, null); + return this._attributeMatches(conceptIdx, refinement, null, opContext); case ECLNodeType.ATTRIBUTE_SET: for (const a of refinement.attributes) { - if (!this._refinementMatches(conceptIdx, a)) return false; + if (!this._refinementMatches(conceptIdx, a, opContext)) return false; } return true; case ECLNodeType.ATTRIBUTE_GROUP: - return this._attributeGroupMatches(conceptIdx, refinement); + return this._attributeGroupMatches(conceptIdx, refinement, opContext); default: throw new Error(`Unsupported refinement node type: ${refinement.type}`); } @@ -804,7 +809,7 @@ class SnomedServices { * @param {number|null} groupFilter * @returns {boolean} */ - _attributeMatches = function (conceptIdx, attr, groupFilter) { + _attributeMatches = function (conceptIdx, attr, groupFilter, opContext) { if (attr.reverse) { throw new Error('ECL reverse attributes (R) are not yet supported'); } @@ -821,7 +826,7 @@ class SnomedServices { throw new Error('ECL refinements only support plain concept-reference attribute names'); } - const count = this._countAttributeMatches(conceptIdx, attr, groupFilter); + const count = this._countAttributeMatches(conceptIdx, attr, groupFilter, opContext); if (attr.cardinality) { return this._cardinalityAccepts(attr.cardinality, count); @@ -838,18 +843,19 @@ class SnomedServices { * @param {number|null} groupFilter * @returns {number} */ - _countAttributeMatches = function (conceptIdx, attr, groupFilter) { + _countAttributeMatches = function (conceptIdx, attr, groupFilter, opContext) { const attrResult = this.concepts.findConcept(attr.name.conceptId); if (!attrResult.found) { throw new Error(`The SNOMED CT Concept ${attr.name.conceptId} is not known`); } const attrTypeIdx = attrResult.index; - const valueSet = new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value))); + const valueSet = new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value, opContext), opContext)); const relIdxs = this.getConceptRelationships(conceptIdx); let count = 0; for (const relIdx of relIdxs) { + if (opContext) opContext.deadCheck('ecl:countAttributeMatches'); const rel = this.relationships.getRelationship(relIdx); if (!rel.active) continue; if (rel.relType !== attrTypeIdx) continue; @@ -884,7 +890,7 @@ class SnomedServices { * @param {object} group * @returns {boolean} */ - _attributeGroupMatches = function (conceptIdx, group) { + _attributeGroupMatches = function (conceptIdx, group, opContext) { const relIdxs = this.getConceptRelationships(conceptIdx); const groupNumbers = new Set(); for (const relIdx of relIdxs) { @@ -896,9 +902,10 @@ class SnomedServices { let matchingGroupCount = 0; for (const g of groupNumbers) { + if (opContext) opContext.deadCheck('ecl:attributeGroup'); let allMatch = true; for (const attr of group.attributes) { - if (!this._attributeMatches(conceptIdx, attr, g)) { + if (!this._attributeMatches(conceptIdx, attr, g, opContext)) { allMatch = false; break; } @@ -939,9 +946,9 @@ class SnomedServices { * @param {SnomedFilterContext} ctx * @returns {number[]} */ - _eclResolveSet = function (ctx) { + _eclResolveSet = function (ctx, opContext) { if (ctx.eclWildcard && (!ctx.descendants || ctx.descendants.length === 0)) { - return this._eclEnumerateActiveConcepts(); + return this._eclEnumerateActiveConcepts(opContext); } return this._eclToIndexArray(ctx); }; @@ -949,20 +956,24 @@ class SnomedServices { /** * AND: concepts present in both sets. */ - _eclIntersect = function (left, right) { + _eclIntersect = function (left, right, opContext) { if (left.eclWildcard) return right; if (right.eclWildcard) return left; const leftSet = new Set(this._eclToIndexArray(left)); const result = new SnomedFilterContext(); - result.descendants = this._eclToIndexArray(right).filter(idx => leftSet.has(idx)); + result.descendants = this._eclToIndexArray(right).filter(idx => { + if (opContext) opContext.deadCheck('ecl:intersect'); + return leftSet.has(idx); + }); return result; }; /** * OR: concepts present in either set. */ - _eclUnion = function (left, right) { + _eclUnion = function (left, right, opContext) { if (left.eclWildcard || right.eclWildcard) return this._eclWildcard(); + if (opContext) opContext.deadCheck('ecl:union'); const combined = new Set([ ...this._eclToIndexArray(left), ...this._eclToIndexArray(right) @@ -975,7 +986,7 @@ class SnomedServices { /** * MINUS: concepts in left that are not in right. */ - _eclMinus = function (left, right) { + _eclMinus = function (left, right, opContext) { const result = new SnomedFilterContext(); if (right.eclWildcard) { @@ -989,6 +1000,7 @@ class SnomedServices { // Enumerate all active concepts minus the right set const all = []; for (let i = 0; i < this.concepts.count(); i++) { + if (opContext) opContext.deadCheck('ecl:minus'); const concept = this.concepts.getConceptByCount(i); if (this.isActive(concept.index) && !rightSet.has(concept.index)) { all.push(concept.index); @@ -998,7 +1010,10 @@ class SnomedServices { return result; } - result.descendants = this._eclToIndexArray(left).filter(idx => !rightSet.has(idx)); + result.descendants = this._eclToIndexArray(left).filter(idx => { + if (opContext) opContext.deadCheck('ecl:minus'); + return !rightSet.has(idx); + }); return result; }; From 272dc3332c974d2d992dc7c3335b98eb913395f5 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 07:42:59 +1000 Subject: [PATCH 2/6] #239: fix serialising of version in used-fragment --- tx/workers/expand.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tx/workers/expand.js b/tx/workers/expand.js index 1992b0e7..7598702c 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -647,11 +647,11 @@ class ValueSetExpander { } else if (cs.contentMode() === 'supplement') { throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' defines a supplement, so this expansion cannot be performed', 'invalid'); } else if (cs.contentMode() === 'fragment') { - this.addParamUri(exp, 'used-fragment', cs.system() + '|' + cs.version()); + this.addParamUri(exp, 'used-fragment', cs.system()+ (cs.version() ? '|' + cs.version(): "")); Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason","This extension is based on a fragment of the code system " + cset.system); } else { - this.addParamUri(exp, cs.contentMode(), cs.system() + '|' + cs.version()); + this.addParamUri(exp, cs.contentMode(), cs.system() + cs.system()+ (cs.version() ? '|' + cs.version(): "")); Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason","This extension is based on a fragment of the code system " + cset.system); } From 43dbd73d2181e7538134f5ba0d6027dee6c41409 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 07:53:20 +1000 Subject: [PATCH 3/6] #237 error handling unknown code system in exclude --- tx/workers/expand.js | 190 ++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 93 deletions(-) diff --git a/tx/workers/expand.js b/tx/workers/expand.js index 7598702c..dfc0f00a 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -961,124 +961,128 @@ class ValueSetExpander { const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, true, true, null, this.requiredSupplements); - this.worker.checkSupplements(cs, cset, this.requiredSupplements, this.usedSupplements); - this.checkResourceCanonicalStatus(expansion, cs, this.valueSet); - const sv = this.canonical(await cs.system(), await cs.version()); - this.addParamUri(expansion, 'used-codesystem', sv); + if (cs == null) { + // nothing? + } else { + this.worker.checkSupplements(cs, cset, this.requiredSupplements, this.usedSupplements); + this.checkResourceCanonicalStatus(expansion, cs, this.valueSet); + const sv = this.canonical(await cs.system(), await cs.version()); + this.addParamUri(expansion, 'used-codesystem', sv); - for (const u of cset.valueSet || []) { - this.worker.deadCheck('processCodes#3'); - const s = this.worker.pinValueSet(u); - this.worker.opContext.log('import value set ' + s); - let vs = await this.worker.findValueSet(s, '', vsSrc); - const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed)); - this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet); - if (!vs.isContained) { - this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet)); + for (const u of cset.valueSet || []) { + this.worker.deadCheck('processCodes#3'); + const s = this.worker.pinValueSet(u); + this.worker.opContext.log('import value set ' + s); + let vs = await this.worker.findValueSet(s, '', vsSrc); + const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed)); + this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet); + if (!vs.isContained) { + this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet)); + } + valueSets.push(ivs); } - valueSets.push(ivs); - } - if (!cset.concept && !cset.filter) { - this.worker.opContext.log('handle system'); - if (!cset.valueSet) { - if (!this.excludeSpecialCase) { - // excluding a whole system - we don't list the codes in this case - this.excludedSystems.add(cset.system + (this.doingVersion && cset.version ? '|' + cset.version : '')); + if (!cset.concept && !cset.filter) { + this.worker.opContext.log('handle system'); + if (!cset.valueSet) { + if (!this.excludeSpecialCase) { + // excluding a whole system - we don't list the codes in this case + this.excludedSystems.add(cset.system + (this.doingVersion && cset.version ? '|' + cset.version : '')); + } else { + const iter = await cs.iteratorAll(); + if (iter) { + let c = await cs.nextContext(iter); + while (c) { + this.worker.deadCheck('processCodes#3aa'); + this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url); + c = await cs.nextContext(iter); + } + } + } } else { + if (cs.isNotClosed(filter)) { + if (cs.specialEnumeration()) { + Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); + 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()); + } else { + throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics()); + } + } + const iter = await cs.iteratorAll(); if (iter) { let c = await cs.nextContext(iter); while (c) { - this.worker.deadCheck('processCodes#3aa'); - this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url); + this.worker.deadCheck('processCodes#3a'); + if (await this.passesFilters(cs, c, prep, filters, 0)) { + this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url); + } c = await cs.nextContext(iter); } } } - } else { - if (cs.isNotClosed(filter)) { - if (cs.specialEnumeration()) { - Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); - 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()); - } else { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - const iter = await cs.iteratorAll(); - if (iter) { - let c = await cs.nextContext(iter); - while (c) { - this.worker.deadCheck('processCodes#3a'); - if (await this.passesFilters(cs, c, prep, filters, 0)) { - this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url); - } - c = await cs.nextContext(iter); - } - } } - } - if (cset.concept) { - this.worker.opContext.log('iterate concepts'); - const cds = new Designations(this.worker.i18n.languageDefinitions); - for (const cc of cset.concept) { - this.worker.deadCheck('processCodes#3'); - cds.clear(); - Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl); - const cctxt = await cs.locate(cc.code, this.allAltCodes); - if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt.context)) && await this.passesFilters(cs, cctxt.context, prep, filters, 0)) { - if (filter.passesDesignations(cds) || filter.passes(cc.code)) { - let ov = Extensions.readString(cc, 'http://hl7.org/fhir/StructureDefinition/itemWeight'); - if (!ov) { - ov = await cs.itemWeight(cctxt.context); + if (cset.concept) { + this.worker.opContext.log('iterate concepts'); + const cds = new Designations(this.worker.i18n.languageDefinitions); + for (const cc of cset.concept) { + this.worker.deadCheck('processCodes#3'); + cds.clear(); + Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl); + const cctxt = await cs.locate(cc.code, this.allAltCodes); + if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt.context)) && await this.passesFilters(cs, cctxt.context, prep, filters, 0)) { + if (filter.passesDesignations(cds) || filter.passes(cc.code)) { + let ov = Extensions.readString(cc, 'http://hl7.org/fhir/StructureDefinition/itemWeight'); + if (!ov) { + ov = await cs.itemWeight(cctxt.context); + } + this.excludeCode(cs, await cs.system(), await cs.version(), cc.code, expansion, valueSets, vsSrc.url); } - this.excludeCode(cs, await cs.system(), await cs.version(), cc.code, expansion, valueSets, vsSrc.url); } } } - } - if (cset.filter) { - this.worker.opContext.log('prep filters'); - const prep = await cs.getPrepContext(true); - if (!filter.isNull) { - await cs.searchFilter(filter, prep, true); - } + if (cset.filter) { + this.worker.opContext.log('prep filters'); + const prep = await cs.getPrepContext(true); + if (!filter.isNull) { + await cs.searchFilter(filter, prep, true); + } - if (cs.specialEnumeration()) { - await cs.specialFilter(prep, true); - Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); - 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()); - } + if (cs.specialEnumeration()) { + await cs.specialFilter(prep, true); + Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); + 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()); + } - let first = true; - for (let fc of cset.filter) { - this.worker.deadCheck('processCodes#4a'); - Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl); - await cs.filter(prep, first, fc.property, fc.op, fc.value); - first = false; - } + let first = true; + for (let fc of cset.filter) { + this.worker.deadCheck('processCodes#4a'); + Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl); + await cs.filter(prep, first, fc.property, fc.op, fc.value); + first = false; + } - this.worker.opContext.log('iterate filters'); - const fset = await cs.executeFilters(prep); - if (await cs.filtersNotClosed(prep)) { - notClosed.value = true; - } - //let count = 0; - while (await cs.filterMore(prep, fset[0])) { - this.worker.deadCheck('processCodes#5'); - const c = await cs.filterConcept(prep, fset[0]); - const ok = (!this.params.activeOnly || !await cs.isInactive(c)) && (await this.passesFilters(cs, c, prep, fset, 1)); - if (ok) { - //count++; - if (this.passesImports(valueSets, await cs.system(), await cs.code(c), 0)) { - this.excludeCode(cs, await cs.system(), await cs.version(), await cs.code(c), expansion, null, vsSrc.url); + this.worker.opContext.log('iterate filters'); + const fset = await cs.executeFilters(prep); + if (await cs.filtersNotClosed(prep)) { + notClosed.value = true; + } + //let count = 0; + while (await cs.filterMore(prep, fset[0])) { + this.worker.deadCheck('processCodes#5'); + const c = await cs.filterConcept(prep, fset[0]); + const ok = (!this.params.activeOnly || !await cs.isInactive(c)) && (await this.passesFilters(cs, c, prep, fset, 1)); + if (ok) { + //count++; + if (this.passesImports(valueSets, await cs.system(), await cs.code(c), 0)) { + this.excludeCode(cs, await cs.system(), await cs.version(), await cs.code(c), expansion, null, vsSrc.url); + } } } + this.worker.opContext.log('iterate filters finished'); } - this.worker.opContext.log('iterate filters finished'); } } } From 515973f4f2ac3ed251d3857429af5a4adc31bbba Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 17:23:16 +1000 Subject: [PATCH 4/6] #236 error with inferSystem + a nested-valueSet exclude --- tx/workers/expand.js | 2 +- tx/workers/validate.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tx/workers/expand.js b/tx/workers/expand.js index dfc0f00a..96e89967 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -948,7 +948,7 @@ class ValueSetExpander { let vs = await this.worker.findValueSet(s, '', vsSrc); const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed)); this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet); - if (!vs.isContained) { + if (!vs.isContained && ivs.valueSet.vurl) { this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl); } valueSets.push(ivs); diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 8391df0e..966b75b1 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -124,7 +124,7 @@ class ValueSetChecker { } } } catch (error) { - this.log.error(error); + this.worker.log.error(error); debugLog(error); throw new Error('Exception expanding value set in order to infer system: ' + error.message); } From d430f58857aaf12b0113c23ba8c918ccf7a8ba78 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 17:32:02 +1000 Subject: [PATCH 5/6] #246 ConceptMap read 404 reports "ValueSet/{id} not found --- tx/workers/read.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tx/workers/read.js b/tx/workers/read.js index e0fd04e0..99286d71 100644 --- a/tx/workers/read.js +++ b/tx/workers/read.js @@ -179,7 +179,7 @@ class ReadWorker extends TerminologyWorker { issue: [{ severity: 'error', code: 'not-found', - diagnostics: `ValueSet/${id} not found` + diagnostics: `ConceptMap/${id} not found` }] }); } From 25ed166e28b0d2d1bfb489771fb881ec21a45657 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 7 Jun 2026 20:21:28 +1000 Subject: [PATCH 6/6] #245 Instance reads (GET /{type}/{id}) ignore Accept: application/fhir+xml and return JSON --- tx/tx.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tx/tx.js b/tx/tx.js index 28994f83..89b30947 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -1131,8 +1131,9 @@ class TXModule { convertResourceToXml(res) { switch (res.resourceType) { - case "CodeSystem" : return CodeSystemXML._jsonToXml(res); + case "CodeSystem" : return CodeSystemXML.toXml(res); case "ValueSet" : return ValueSetXML.toXml(res); + case "ConceptMap" : return ConceptMapXML.toXml(res); case "Bundle" : return BundleXML.toXml(res, this.fhirVersion); case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5"); case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");