From e99a95c9f7cbc957eb054f0eb826477cbcde286f Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:05:48 +1000 Subject: [PATCH 1/7] deal with regex Denial of Service Issue --- library/regex-utilities.js | 13 +++++++++++++ tests/cs/cs-cs.test.js | 2 +- translations/Messages.properties | 1 + tx/cs/cs-country.js | 3 ++- tx/cs/cs-cs.js | 7 ++++--- tx/cs/cs-loinc.js | 3 ++- tx/library/ucum-parsers.js | 3 ++- tx/ocl/cs-ocl.cjs | 3 ++- 8 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 library/regex-utilities.js diff --git a/library/regex-utilities.js b/library/regex-utilities.js new file mode 100644 index 00000000..8a0920b9 --- /dev/null +++ b/library/regex-utilities.js @@ -0,0 +1,13 @@ +const { RE2 } = require('re2-wasm'); + +class RegExUtilities { + + compile(pattern, flags) { + // RE2 requires the unicode flag; add it if not already present + const re2Flags = flags && flags.includes('u') ? flags : (flags || '') + 'u'; + return new RE2(pattern, re2Flags); + } + +} + +module.exports = new RegExUtilities(); diff --git a/tests/cs/cs-cs.test.js b/tests/cs/cs-cs.test.js index f8ad39c7..3027ebd6 100644 --- a/tests/cs/cs-cs.test.js +++ b/tests/cs/cs-cs.test.js @@ -1520,7 +1520,7 @@ describe('FHIR CodeSystem Provider', () => { test('should handle invalid regex gracefully', async () => { await expect( simpleProvider.filter(filterContext, 'code', 'regex', '[invalid') - ).rejects.toThrow('Invalid regex pattern'); + ).rejects.toThrow('The regex \'[invalid\' is not valid: Invalid regular expression: /^[invalid$/u: missing ]: [invalid$'); }); }); diff --git a/translations/Messages.properties b/translations/Messages.properties index 38d2391f..326f90c1 100644 --- a/translations/Messages.properties +++ b/translations/Messages.properties @@ -1511,3 +1511,4 @@ CONFORMANCE_STATEMENT_WORD = The html source contains the word ''{0}'' but it is VALUESET_CODE_CONCEPT_HINT = {3}. Note that the display in the ValueSet does not have to match; this check exists to help check that it''s not accidentally the wrong code VALUESET_CODE_CONCEPT_HINT_VER ={3}. Note that the display in the ValueSet does not have to match; this check exists to help check that it''s not accidentally the wrong code TERMINOLOGY_TX_SYSTEM_UNSUPPORTED = The code cannot be checked because codeSystem ''{0}'' version ''{1}'' is not supported ({2}) +INVALID_REGEX = The regex ''{0}'' is not valid: {1} \ No newline at end of file diff --git a/tx/cs/cs-country.js b/tx/cs/cs-country.js index 27c2a21b..3c1b6057 100644 --- a/tx/cs/cs-country.js +++ b/tx/cs/cs-country.js @@ -2,6 +2,7 @@ const { CodeSystemProvider, FilterExecutionContext } = require('../../tx/cs/cs-a const assert = require('assert'); const { CodeSystem } = require("../library/codesystem"); const {CodeSystemFactoryProvider} = require("./cs-api"); +const regexUtilities = require("../../library/regex-utilities"); class CountryCodeConcept { constructor(userDefined, code, display, french) { @@ -199,7 +200,7 @@ class CountryCodeServices extends CodeSystemProvider { try { // Create regex with anchors to match the Pascal implementation (^value$) - const regex = new RegExp('^' + value + '$'); + const regex = regexUtilities.compile('^' + value + '$'); for (const concept of this.codes) { if (regex.test(concept.code)) { diff --git a/tx/cs/cs-cs.js b/tx/cs/cs-cs.js index 8ac2c669..3a9721c6 100644 --- a/tx/cs/cs-cs.js +++ b/tx/cs/cs-cs.js @@ -6,6 +6,7 @@ const { validateOptionalParameter, getValuePrimitive, validateArrayParameter} = const {Issue} = require("../library/operation-outcome"); const {Extensions} = require("../library/extensions"); const {BaseCSServices} = require("./cs-base"); +const regexUtilities = require("../../library/regex-utilities"); /** * Context class for FHIR CodeSystem provider concepts @@ -1309,7 +1310,7 @@ class FhirCodeSystemProvider extends BaseCSServices { else if (op === 'regex') { // Regular expression match try { - const regex = new RegExp('^' + value + '$'); + const regex = regexUtilities.compile('^' + value + '$'); const allCodes = this.codeSystem.getAllCodes(); for (const code of allCodes) { if (regex.test(code)) { @@ -1320,7 +1321,7 @@ class FhirCodeSystemProvider extends BaseCSServices { } } } catch (error) { - throw new Error(`Invalid regex pattern: ${value}`); + throw new Issue('error', 'exception', null, 'INVALID_REGEX', this.opContext.i18n.translate('INVALID_REGEX', this.opContext.langs, [value, error.message]), 'vs-invalid', 422); } } @@ -1428,7 +1429,7 @@ class FhirCodeSystemProvider extends BaseCSServices { } else if (op === 'regex') { try { - const regex = new RegExp('^' + value + '$'); + const regex = regexUtilities.compile('^' + value + '$'); return properties.some(p => regex.test(this._getPropertyValue(p))); } catch (error) { return false; diff --git a/tx/cs/cs-loinc.js b/tx/cs/cs-loinc.js index 338f68eb..b5020aab 100644 --- a/tx/cs/cs-loinc.js +++ b/tx/cs/cs-loinc.js @@ -6,6 +6,7 @@ const { CodeSystemFactoryProvider} = require('./cs-api'); const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities"); const {BaseCSServices} = require("./cs-base"); const {sqlEscapeString} = require("../../xig/xig"); +const regexUtilities = require('../../library/regex-utilities'); // Context kinds matching Pascal enum const LoincProviderContextKind = { @@ -938,7 +939,7 @@ class LoincServices extends BaseCSServices { // Helper method for regex matching async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') { return new Promise((resolve, reject) => { - const regex = new RegExp(pattern); + const regex = regexUtilities.compile(pattern); const matchingKeys = []; this.db.all(sql, (err, rows) => { diff --git a/tx/library/ucum-parsers.js b/tx/library/ucum-parsers.js index 15e74c14..2aebb5f6 100644 --- a/tx/library/ucum-parsers.js +++ b/tx/library/ucum-parsers.js @@ -10,6 +10,7 @@ const { BaseUnit, DefinedUnit, Prefix, Value, Term, Symbol, Factor, Canonical, CanonicalUnit, Registry } = require('./ucum-types.js'); +const regexUtilities = require("../../library/regex-utilities"); // Lexer for tokenizing UCUM expressions (port of Lexer.java) class Lexer { @@ -763,7 +764,7 @@ class Search { if (isRegex) { try { - const regex = new RegExp(text); + const regex = regexUtilities.compile(text); return regex.test(value); } catch (e) { this.log.error(e); diff --git a/tx/ocl/cs-ocl.cjs b/tx/ocl/cs-ocl.cjs index 2d3e9750..06658528 100644 --- a/tx/ocl/cs-ocl.cjs +++ b/tx/ocl/cs-ocl.cjs @@ -13,6 +13,7 @@ const { OCLBackgroundJobQueue } = require('./jobs/background-queue'); const { OCLConceptFilterContext } = require('./model/concept-filter-context'); const { toConceptContext } = require('./mappers/concept-mapper'); const { patchSearchWorkerForOCLCodeFiltering } = require('./shared/patches'); +const regexUtilities = require("../../library/regex-utilities"); patchSearchWorkerForOCLCodeFiltering(); @@ -1135,7 +1136,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider { #buildPropertyMatcher(prop, op, value) { if (op === 'regex') { - const regex = new RegExp(String(value), 'i'); + const regex = regexUtilities.compile(String(value), 'i'); return concept => { const candidate = this.#valueForFilter(concept, prop); if (candidate == null) { From caeba79c394d2114e1ed5cb41046cb2c3f32e1c5 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:06:20 +1000 Subject: [PATCH 2/7] fix error in SNOMED translate --- package-lock.json | 10 ++ package.json | 3 +- tests/tx/test-cases.test.js | 334 ++++++++++++++++++++++++++++++++++++ 3 files changed, 346 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 0aac1154..fc64305d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "properties-file": "^3.6.4", + "re2-wasm": "^1.0.2", "rimraf": "^5.0.10", "sqlite3": "^5.1.7", "tar": "^7.5.7", @@ -8043,6 +8044,15 @@ "node": ">=0.10.0" } }, + "node_modules/re2-wasm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/re2-wasm/-/re2-wasm-1.0.2.tgz", + "integrity": "sha512-VXUdgSiUrE/WZXn6gUIVVIsg0+Hp6VPZPOaHCay+OuFKy6u/8ktmeNEf+U5qSA8jzGGFsg8jrDNu1BeHpz2pJA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 478e0234..a3bebf3f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fhirsmith", "version": "0.8.6", - "txVersion" : "1.9.1", + "txVersion": "1.9.1", "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem", "main": "server.js", "engines": { @@ -61,6 +61,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "properties-file": "^3.6.4", + "re2-wasm": "^1.0.2", "rimraf": "^5.0.10", "sqlite3": "^5.1.7", "tar": "^7.5.7", diff --git a/tests/tx/test-cases.test.js b/tests/tx/test-cases.test.js index 88508ff7..4392eb87 100644 --- a/tests/tx/test-cases.test.js +++ b/tests/tx/test-cases.test.js @@ -3220,6 +3220,14 @@ describe('overload', () => { describe('fragment', () => { // Testing handling a code system fragment + it('fragment-expansionR5', async () => { + await runTest({"suite":"fragment","test":"fragment-expansion"}, "5.0"); + }); + + it('fragment-expansionR4', async () => { + await runTest({"suite":"fragment","test":"fragment-expansion"}, "4.0"); + }); + it('validation-fragment-code-goodR5', async () => { await runTest({"suite":"fragment","test":"validation-fragment-code-good"}, "5.0"); }); @@ -4064,6 +4072,18 @@ describe('translate', () => { await runTest({"suite":"translate","test":"translate-1"}, "5.0"); }); + it('translate-1R4', async () => { + await runTest({"suite":"translate","test":"translate-1"}, "4.0"); + }); + + it('translate-reverseR5', async () => { + await runTest({"suite":"translate","test":"translate-reverse"}, "5.0"); + }); + + it('translate-reverseR4', async () => { + await runTest({"suite":"translate","test":"translate-reverse"}, "4.0"); + }); + }); describe('tho', () => { @@ -4361,6 +4381,14 @@ describe('tx.fhir.org', () => { await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code"}, "4.0"); }); + it('loinc-validate-code-uzR5', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code-uz"}, "5.0"); + }); + + it('loinc-validate-code-uzR4', async () => { + await runTest({"suite":"tx.fhir.org","test":"loinc-validate-code-uz"}, "4.0"); + }); + it('loinc-validate-discouraged-codeR5', async () => { await runTest({"suite":"tx.fhir.org","test":"loinc-validate-discouraged-code"}, "5.0"); }); @@ -4785,6 +4813,14 @@ describe('tx.fhir.org', () => { await runTest({"suite":"tx.fhir.org","test":"snomed-expand-property-1"}, "4.0"); }); + it('snomed-expand-property-2R5', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-property-2"}, "5.0"); + }); + + it('snomed-expand-property-2R4', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-expand-property-2"}, "4.0"); + }); + it('snomed-validate-active-badR5', async () => { await runTest({"suite":"tx.fhir.org","test":"snomed-validate-active-bad"}, "5.0"); }); @@ -4849,6 +4885,14 @@ describe('tx.fhir.org', () => { await runTest({"suite":"tx.fhir.org","test":"snomed-validate-property-good"}, "4.0"); }); + it('snomed-translateR5', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-translate"}, "5.0"); + }); + + it('snomed-translateR4', async () => { + await runTest({"suite":"tx.fhir.org","test":"snomed-translate"}, "4.0"); + }); + }); describe('snomed', () => { @@ -6150,5 +6194,295 @@ describe('permutations', () => { }); +describe('regex-bad', () => { + // Bad Regex - denial of service attack + + it('expand-regex-badR5', async () => { + await runTest({"suite":"regex-bad","test":"expand-regex-bad"}, "5.0"); + }); + + it('expand-regex-badR4', async () => { + await runTest({"suite":"regex-bad","test":"expand-regex-bad"}, "4.0"); + }); + + it('validate-regex-badR5', async () => { + await runTest({"suite":"regex-bad","test":"validate-regex-bad"}, "5.0"); + }); + + it('validate-regex-badR4', async () => { + await runTest({"suite":"regex-bad","test":"validate-regex-bad"}, "4.0"); + }); + +}); + +describe('related2', () => { + // Tests for $compare operation - comparing two value sets to determine their relationship (equivalent, subset, superset, overlap, disjoint, unknown) + + it('related-eq-identical-defR5', async () => { + await runTest({"suite":"related2","test":"related-eq-identical-def"}, "5.0"); + }); + + it('related-eq-identical-defR4', async () => { + await runTest({"suite":"related2","test":"related-eq-identical-def"}, "4.0"); + }); + + it('related-eq-enum-reorderR5', async () => { + await runTest({"suite":"related2","test":"related-eq-enum-reorder"}, "5.0"); + }); + + it('related-eq-enum-reorderR4', async () => { + await runTest({"suite":"related2","test":"related-eq-enum-reorder"}, "4.0"); + }); + + it('related-eq-multi-include-reorderR5', async () => { + await runTest({"suite":"related2","test":"related-eq-multi-include-reorder"}, "5.0"); + }); + + it('related-eq-multi-include-reorderR4', async () => { + await runTest({"suite":"related2","test":"related-eq-multi-include-reorder"}, "4.0"); + }); + + it('related-eq-filter-vs-enumR5', async () => { + await runTest({"suite":"related2","test":"related-eq-filter-vs-enum"}, "5.0"); + }); + + it('related-eq-filter-vs-enumR4', async () => { + await runTest({"suite":"related2","test":"related-eq-filter-vs-enum"}, "4.0"); + }); + + it('related-eq-import-vs-inlineR5', async () => { + await runTest({"suite":"related2","test":"related-eq-import-vs-inline"}, "5.0"); + }); + + it('related-eq-import-vs-inlineR4', async () => { + await runTest({"suite":"related2","test":"related-eq-import-vs-inline"}, "4.0"); + }); + + it('related-eq-import-reorderR5', async () => { + await runTest({"suite":"related2","test":"related-eq-import-reorder"}, "5.0"); + }); + + it('related-eq-import-reorderR4', async () => { + await runTest({"suite":"related2","test":"related-eq-import-reorder"}, "4.0"); + }); + + it('related-expeq-exclude-vs-enumR5', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-vs-enum"}, "5.0"); + }); + + it('related-expeq-exclude-vs-enumR4', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-vs-enum"}, "4.0"); + }); + + it('related-expeq-exclude-partialR5', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-partial"}, "5.0"); + }); + + it('related-expeq-exclude-partialR4', async () => { + await runTest({"suite":"related2","test":"related-expeq-exclude-partial"}, "4.0"); + }); + + it('related-sub-branch-vs-rootR5', async () => { + await runTest({"suite":"related2","test":"related-sub-branch-vs-root"}, "5.0"); + }); + + it('related-sub-branch-vs-rootR4', async () => { + await runTest({"suite":"related2","test":"related-sub-branch-vs-root"}, "4.0"); + }); + + it('related-sub-enum-vs-filterR5', async () => { + await runTest({"suite":"related2","test":"related-sub-enum-vs-filter"}, "5.0"); + }); + + it('related-sub-enum-vs-filterR4', async () => { + await runTest({"suite":"related2","test":"related-sub-enum-vs-filter"}, "4.0"); + }); + + it('related-sub-base-vs-import-plusR5', async () => { + await runTest({"suite":"related2","test":"related-sub-base-vs-import-plus"}, "5.0"); + }); + + it('related-sub-base-vs-import-plusR4', async () => { + await runTest({"suite":"related2","test":"related-sub-base-vs-import-plus"}, "4.0"); + }); + + it('related-sub-leaf-vs-subtreeR5', async () => { + await runTest({"suite":"related2","test":"related-sub-leaf-vs-subtree"}, "5.0"); + }); + + it('related-sub-leaf-vs-subtreeR4', async () => { + await runTest({"suite":"related2","test":"related-sub-leaf-vs-subtree"}, "4.0"); + }); + + it('related-super-root-vs-branchR5', async () => { + await runTest({"suite":"related2","test":"related-super-root-vs-branch"}, "5.0"); + }); + + it('related-super-root-vs-branchR4', async () => { + await runTest({"suite":"related2","test":"related-super-root-vs-branch"}, "4.0"); + }); + + it('related-expsub-exclude-narrowerR5', async () => { + await runTest({"suite":"related2","test":"related-expsub-exclude-narrower"}, "5.0"); + }); + + it('related-expsub-exclude-narrowerR4', async () => { + await runTest({"suite":"related2","test":"related-expsub-exclude-narrower"}, "4.0"); + }); + + it('related-disj-diff-systemsR5', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-systems"}, "5.0"); + }); + + it('related-disj-diff-systemsR4', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-systems"}, "4.0"); + }); + + it('related-disj-diff-branchesR5', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-branches"}, "5.0"); + }); + + it('related-disj-diff-branchesR4', async () => { + await runTest({"suite":"related2","test":"related-disj-diff-branches"}, "4.0"); + }); + + it('related-disj-enum-no-intersectionR5', async () => { + await runTest({"suite":"related2","test":"related-disj-enum-no-intersection"}, "5.0"); + }); + + it('related-disj-enum-no-intersectionR4', async () => { + await runTest({"suite":"related2","test":"related-disj-enum-no-intersection"}, "4.0"); + }); + + it('related-disj-multi-systemR5', async () => { + await runTest({"suite":"related2","test":"related-disj-multi-system"}, "5.0"); + }); + + it('related-disj-multi-systemR4', async () => { + await runTest({"suite":"related2","test":"related-disj-multi-system"}, "4.0"); + }); + + it('related-ov-enum-partialR5', async () => { + await runTest({"suite":"related2","test":"related-ov-enum-partial"}, "5.0"); + }); + + it('related-ov-enum-partialR4', async () => { + await runTest({"suite":"related2","test":"related-ov-enum-partial"}, "4.0"); + }); + + it('related-ov-filter-vs-enumR5', async () => { + await runTest({"suite":"related2","test":"related-ov-filter-vs-enum"}, "5.0"); + }); + + it('related-ov-filter-vs-enumR4', async () => { + await runTest({"suite":"related2","test":"related-ov-filter-vs-enum"}, "4.0"); + }); + + it('related-ov-multi-include-partialR5', async () => { + await runTest({"suite":"related2","test":"related-ov-multi-include-partial"}, "5.0"); + }); + + it('related-ov-multi-include-partialR4', async () => { + await runTest({"suite":"related2","test":"related-ov-multi-include-partial"}, "4.0"); + }); + + it('related-ov-import-partialR5', async () => { + await runTest({"suite":"related2","test":"related-ov-import-partial"}, "5.0"); + }); + + it('related-ov-import-partialR4', async () => { + await runTest({"suite":"related2","test":"related-ov-import-partial"}, "4.0"); + }); + + it('related-ov-cross-systemR5', async () => { + await runTest({"suite":"related2","test":"related-ov-cross-system"}, "5.0"); + }); + + it('related-ov-cross-systemR4', async () => { + await runTest({"suite":"related2","test":"related-ov-cross-system"}, "4.0"); + }); + + it('related-ov-exclude-partialR5', async () => { + await runTest({"suite":"related2","test":"related-ov-exclude-partial"}, "5.0"); + }); + + it('related-ov-exclude-partialR4', async () => { + await runTest({"suite":"related2","test":"related-ov-exclude-partial"}, "4.0"); + }); + + it('related-unk-snomed-both-filterR5', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-both-filter"}, "5.0"); + }); + + it('related-unk-snomed-both-filterR4', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-both-filter"}, "4.0"); + }); + + it('related-unk-snomed-filter-vs-enumR5', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-filter-vs-enum"}, "5.0"); + }); + + it('related-unk-snomed-filter-vs-enumR4', async () => { + await runTest({"suite":"related2","test":"related-unk-snomed-filter-vs-enum"}, "4.0"); + }); + + it('related-unk-unknown-systemR5', async () => { + await runTest({"suite":"related2","test":"related-unk-unknown-system"}, "5.0"); + }); + + it('related-unk-unknown-systemR4', async () => { + await runTest({"suite":"related2","test":"related-unk-unknown-system"}, "4.0"); + }); + + it('related-ver-same-def-diff-cs-versionR5', async () => { + await runTest({"suite":"related2","test":"related-ver-same-def-diff-cs-version"}, "5.0"); + }); + + it('related-ver-same-def-diff-cs-versionR4', async () => { + await runTest({"suite":"related2","test":"related-ver-same-def-diff-cs-version"}, "4.0"); + }); + + it('related-ver-all-diff-cs-versionR5', async () => { + await runTest({"suite":"related2","test":"related-ver-all-diff-cs-version"}, "5.0"); + }); + + it('related-ver-all-diff-cs-versionR4', async () => { + await runTest({"suite":"related2","test":"related-ver-all-diff-cs-version"}, "4.0"); + }); + + it('related-ver-branch-diff-cs-versionR5', async () => { + await runTest({"suite":"related2","test":"related-ver-branch-diff-cs-version"}, "5.0"); + }); + + it('related-ver-branch-diff-cs-versionR4', async () => { + await runTest({"suite":"related2","test":"related-ver-branch-diff-cs-version"}, "4.0"); + }); + + it('related-ver-unversioned-vs-pinnedR5', async () => { + await runTest({"suite":"related2","test":"related-ver-unversioned-vs-pinned"}, "5.0"); + }); + + it('related-ver-unversioned-vs-pinnedR4', async () => { + await runTest({"suite":"related2","test":"related-ver-unversioned-vs-pinned"}, "4.0"); + }); + + it('related-ver-same-vs-diff-versionR5', async () => { + await runTest({"suite":"related2","test":"related-ver-same-vs-diff-version"}, "5.0"); + }); + + it('related-ver-same-vs-diff-versionR4', async () => { + await runTest({"suite":"related2","test":"related-ver-same-vs-diff-version"}, "4.0"); + }); + + it('related-ver-import-version-cascadeR5', async () => { + await runTest({"suite":"related2","test":"related-ver-import-version-cascade"}, "5.0"); + }); + + it('related-ver-import-version-cascadeR4', async () => { + await runTest({"suite":"related2","test":"related-ver-import-version-cascade"}, "4.0"); + }); + +}); + }); From 72c169a55721f3684cb4a08e4b15d1aaf544146f Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:06:44 +1000 Subject: [PATCH 3/7] reduce snomed load set - moved to affiliate managed servers --- tx/cs/cs-snomed.js | 2 +- tx/tx.fhir.org.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tx/cs/cs-snomed.js b/tx/cs/cs-snomed.js index 92472224..be867424 100644 --- a/tx/cs/cs-snomed.js +++ b/tx/cs/cs-snomed.js @@ -1622,7 +1622,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider { internalSource : this, relationship: relationship, id : id, - url: `${this.system}?fhir_cm=${id}`, + url: `${this.system()}?fhir_cm=${id}`, version: this.version(), name: `SNOMED CT ${name} Concept Map`, description: `The concept map implicitly defined by the ${name} Association Reference Set`, diff --git a/tx/tx.fhir.org.yml b/tx/tx.fhir.org.yml index 7fb345c2..183bbfd4 100644 --- a/tx/tx.fhir.org.yml +++ b/tx/tx.fhir.org.yml @@ -18,11 +18,11 @@ sources: - unii:unii_20240622.db - snomed:sct_intl_20240201.cache - snomed!:sct_intl_20250201.cache - - snomed:sct_se_20231130.cache - - snomed:sct_au_20230731.cache - - snomed:sct_be_20231115.cache +# - snomed:sct_se_20231130.cache +# - snomed:sct_au_20230731.cache +# - snomed:sct_be_20231115.cache - snomed:sct_ch_20230607.cache - - snomed:sct_dk_20250930.cache +# - snomed:sct_dk_20250930.cache - snomed:sct_ips_20241216.cache - snomed:sct_nl_20240930.cache - snomed:sct_uk_20230412.cache From b192c4e0e202b569d6495a57f9295abfc1273564 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:07:03 +1000 Subject: [PATCH 4/7] improve fragment handling --- tx/vs/vs-vsac.js | 1 - tx/workers/expand.js | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tx/vs/vs-vsac.js b/tx/vs/vs-vsac.js index 8acee74c..faced58d 100644 --- a/tx/vs/vs-vsac.js +++ b/tx/vs/vs-vsac.js @@ -78,7 +78,6 @@ class VSACValueSetProvider extends AbstractValueSetProvider { if (this.valueSetMap.size == 0) { await this.refreshValueSets(); } - await this.refreshValueSets(); // TODO: remove this // Start periodic refresh this._startRefreshTimer(); this.initialized = true; diff --git a/tx/workers/expand.js b/tx/workers/expand.js index f287bfb6..a45fd5da 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -646,6 +646,10 @@ class ValueSetExpander { throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' has no content, so this expansion cannot be performed', 'invalid'); } 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()); + 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()); Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true); From 97151ac38df3dff048a978a067bdf0ce6c7e56a7 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:07:17 +1000 Subject: [PATCH 5/7] fix property group extension --- .gitignore | 2 ++ tx/importers/atc-to-fhir.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 07bb985a..17a7aeef 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,5 @@ test-cases-summary.txt tx/data/CodeSystem-cpt.db coverage/ + +.claude/ diff --git a/tx/importers/atc-to-fhir.js b/tx/importers/atc-to-fhir.js index d1652b24..6833677a 100644 --- a/tx/importers/atc-to-fhir.js +++ b/tx/importers/atc-to-fhir.js @@ -6,7 +6,7 @@ const ATC_FILE = process.argv[2] || '2025_ATC.xml'; const DDD_FILE = process.argv[3] || '2025_ATC_ddd.xml'; const OUTPUT_FILE = process.argv[4] || 'atc-codesystem.json'; -const PROPERTY_GROUP_EXT_URL = 'http://hl7.org/fhir/property.group'; +const PROPERTY_GROUP_EXT_URL = 'http://hl7.org/fhir/StructureDefinition/Codesystem-property-group'; // Parse XML files function parseXML(filePath) { From 7c36cfe88ddb414ac422eb0a30f39de959ed12a7 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:20:11 +1000 Subject: [PATCH 6/7] fix lint issue --- tx/importers/atc-to-fhir.js | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tx/importers/atc-to-fhir.js b/tx/importers/atc-to-fhir.js index 6833677a..03ebe4b8 100644 --- a/tx/importers/atc-to-fhir.js +++ b/tx/importers/atc-to-fhir.js @@ -277,32 +277,7 @@ try { console.log(`Writing to ${OUTPUT_FILE}...`); fs.writeFileSync(OUTPUT_FILE, JSON.stringify(codeSystem, null, 2)); console.log('Done!'); - - // Print some stats - function countConcepts(concepts) { - let count = 0; - for (const c of concepts) { - count++; - if (c.concept) { - count += countConcepts(c.concept); - } - } - return count; - } - - function countWithDDD(concepts) { - let count = 0; - for (const c of concepts) { - if (c.property?.some(p => p.code === 'dddValue')) { - count++; - } - if (c.concept) { - count += countWithDDD(c.concept); - } - } - return count; - } - + const totalConcepts = countConcepts(codeSystem.concept); const withDDD = countWithDDD(codeSystem.concept); console.log(`\nStatistics:`); @@ -314,3 +289,28 @@ try { console.error('Error:', error.message); process.exit(1); } + +// Print some stats +function countConcepts(concepts) { + let count = 0; + for (const c of concepts) { + count++; + if (c.concept) { + count += countConcepts(c.concept); + } + } + return count; +} + +function countWithDDD(concepts) { + let count = 0; + for (const c of concepts) { + if (c.property?.some(p => p.code === 'dddValue')) { + count++; + } + if (c.concept) { + count += countWithDDD(c.concept); + } + } + return count; +} From 42242c7e11a7431382d428213040d25e9291a80b Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Apr 2026 13:24:17 +1000 Subject: [PATCH 7/7] update liquid --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc64305d..d4cd92bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6131,9 +6131,9 @@ "license": "MIT" }, "node_modules/liquidjs": { - "version": "10.25.0", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.0.tgz", - "integrity": "sha512-XpO7AiGULTG4xcTlwkcTI5JreFG7b6esLCLp+aUSh7YuQErJZEoUXre9u9rbdb0057pfWG4l0VursvLd5Q/eAw==", + "version": "10.25.5", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.5.tgz", + "integrity": "sha512-GKiKeZjJDdVoQAu+S9rzkYsYnYhcep5W3WwZXgb5f+yq484P/k9JqamBbGYu+LBEixcUAXZr2jogdAIjB3ki1w==", "license": "MIT", "dependencies": { "commander": "^10.0.0" diff --git a/package.json b/package.json index a3bebf3f..6791fefc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "fs-extra": "^11.3.3", "ini": "^6.0.0", "inquirer": "^8.2.5", - "liquidjs": "^10.24.0", + "liquidjs": "^10.25.5", "lusca": "^1.7.0", "natural": "^6.12.0", "node-cron": "^3.0.3",