From a46e2df81ea2261a50196c5ba3b21defa6768404 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 8 Feb 2026 23:27:58 +1100 Subject: [PATCH 1/3] QA sweep for comparing old and new servers + add related --- library/languages.js | 42 +- tests/cs/cs-country.test.js | 2 +- tests/library/languages.test.js | 85 +- tests/tx/designations.test.js | 2 +- tests/tx/expand.test.js | 2 +- tests/tx/lookup.test.js | 4 +- tests/tx/subsumes.test.js | 4 +- tx/cs/cs-country.js | 46 + tx/cs/cs-cpt.js | 5 +- tx/cs/cs-ndc.js | 6 + tx/cs/cs-rxnorm.js | 2 +- tx/cs/cs-snomed.js | 4 +- tx/cs/cs-ucum.js | 16 +- tx/library.js | 11 + tx/library/designations.js | 15 +- tx/provider.js | 11 +- tx/sct/expressions.js | 2 +- tx/tx.fhir.org.yml | 4 +- tx/tx.js | 42 + tx/workers/expand.js | 2 +- tx/workers/related.js | 1994 +++++-------------------------- tx/workers/validate.js | 30 +- tx/workers/worker.js | 6 +- 23 files changed, 586 insertions(+), 1751 deletions(-) diff --git a/library/languages.js b/library/languages.js index 3050afa..7434f7f 100644 --- a/library/languages.js +++ b/library/languages.js @@ -120,7 +120,7 @@ class Language { if (index < parts.length) { this.language = parts[index]; if (this.language != '*' && languageDefinitions && !languageDefinitions.languages.has(this.language)) { - throw new Error("The language '"+this.language+"' in the code '"+this.language+"' is not valid"); + throw new Error("The language '"+this.language+"' in the code '"+this.code+"' is not valid"); } index++; } @@ -130,7 +130,7 @@ class Language { const part = parts[index]; if (part.length === 3 && /^[a-zA-Z]{3}$/.test(part)) { if (languageDefinitions && !languageDefinitions.extLanguages.has(part)) { - throw new Error("The extLanguage '"+part+"' in the code '"+code+"' is not valid"); + throw new Error("The extLanguage '"+part+"' in the code '"+this.code+"' is not valid"); } this.extLang.push(part.toLowerCase()); index++; @@ -144,7 +144,7 @@ class Language { const part = parts[index]; if (part.length === 4 && /^[a-zA-Z]{4}$/.test(part)) { if (languageDefinitions && !languageDefinitions.scripts.has(part)) { - throw new Error("The script '"+part+"' in the code '"+code+"' is not valid"); + throw new Error("The script '"+part+"' in the code '"+this.code+"' is not valid"); } this.script = part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); index++; @@ -157,7 +157,7 @@ class Language { if ((part.length === 2 && /^[a-zA-Z]{2}$/.test(part)) || (part.length === 3 && /^[0-9]{3}$/.test(part))) { if (languageDefinitions && !languageDefinitions.regions.has(part)) { - throw new Error("The region '"+part+"' in the code '"+code+"' is not valid"); + throw new Error("The region '"+part+"' in the code '"+this.code+"' is not valid"); } this.region = part.toUpperCase(); index++; @@ -187,9 +187,10 @@ class Language { } else if (part.length === 1 && part !== 'x') { // Extension this.extension = part + '-' + parts.slice(index + 1).join('-'); + index++; break; } else { - index++; + throw new Error("Unable to recognised '"+parts[index]+"' as a valid part in the language code "+this.code); } } } @@ -696,6 +697,37 @@ class LanguageDefinitions { return lang; } + /** + * Parse and validate a language code + * + * @return {Language} parsed language (or null) + */ + parse(code, msg) { + if (!code) { + if (msg) { + msg.message = 'No code provided'; + } + return null; + } + + // Check cache first + if (this.parsed.has(code)) { + return this.parsed.get(code); + } + + try { + const lang = new Language(code, this); + // Cache the result + this.parsed.set(code, lang); + return lang; + } catch (e) { + if (msg) { + msg.message = e.message; + } + return null; + } + } + /** * Get display name for language */ diff --git a/tests/cs/cs-country.test.js b/tests/cs/cs-country.test.js index 38b503e..0795fc2 100644 --- a/tests/cs/cs-country.test.js +++ b/tests/cs/cs-country.test.js @@ -102,7 +102,7 @@ describe('CountryCodeServices', () => { }); test('should return error for invalid codes', async () => { - const invalidCodes = ['XX', 'ZZZ', '999']; + const invalidCodes = ['XX-XX', 'ZZZ', '999']; for (const code of invalidCodes) { const result = await provider.locate(code); diff --git a/tests/library/languages.test.js b/tests/library/languages.test.js index 86e6a97..eaeb1d0 100644 --- a/tests/library/languages.test.js +++ b/tests/library/languages.test.js @@ -216,7 +216,7 @@ describe('Languages Class', () => { }); describe('Language Iteration and Access', () => { - test('should be iterable', () => { + test('should be iteratable', () => { const languages = Languages.fromAcceptLanguage('en-US,fr-CA', this.languageDefinitions); const codes = []; for (const lang of languages) { @@ -361,8 +361,89 @@ Description: Test }); test('should return null for invalid language codes', () => { - const lang = definitions.parse('invalid-US'); + let lang = definitions.parse('invalid-US'); expect(lang).toBeNull(); + const msg = {}; + lang = definitions.parse('en-us', msg); + expect(lang).toBeNull(); + expect(msg.message).toBe("The region 'us' in the code 'en-us' is not valid"); + }); + + test('should parse basic language codes', () => { + const msg = {}; + const lang = definitions.parse('en', msg); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('en'); + expect(msg.message).toBeUndefined(); + }); + + test('should parse wildcard', () => { + const lang = definitions.parse('*', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('*'); + }); + + test('should parse language + region', () => { + const lang = definitions.parse('en-US', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('en'); + expect(lang.region).toBe('US'); + }); + + test('should parse language + script', () => { + const lang = definitions.parse('zh-Hans', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('zh'); + expect(lang.script).toBe('Hans'); + }); + + test('should parse language + script + region', () => { + const lang = definitions.parse('zh-Hans-CN', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('zh'); + expect(lang.script).toBe('Hans'); + expect(lang.region).toBe('CN'); + }); + + test('should parse language + region + variant', () => { + const lang = definitions.parse('sl-IT-nedis', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('sl'); + expect(lang.region).toBe('IT'); + expect(lang.variant).toBe('nedis'); + }); + + test('should parse private use extensions', () => { + const lang = definitions.parse('en-US-x-twain', {}); + expect(lang).not.toBeNull(); + expect(lang.language).toBe('en'); + expect(lang.region).toBe('US'); + }); + + test('should return null for empty code', () => { + const msg = {}; + const lang = definitions.parse('', msg); + expect(lang).toBeNull(); + }); + + test('should return null for invalid language code', () => { + const msg = {}; + const lang = definitions.parse('xx', msg); + expect(lang).toBeNull(); + expect(msg.message).toContain('not valid'); + }); + + test('should return null for unrecognised trailing parts', () => { + const msg = {}; + const lang = definitions.parse('en-ZZ-ZZ', msg); + expect(lang).toBeNull(); + expect(msg.message).toContain('Unable to recognise'); + }); + + test('should cache parsed results', () => { + const a = definitions.parse('en', {}); + const b = definitions.parse('en', {}); + expect(a).toBe(b); }); test('should cache parsed languages', () => { diff --git a/tests/tx/designations.test.js b/tests/tx/designations.test.js index d5e6a06..2a24b99 100644 --- a/tests/tx/designations.test.js +++ b/tests/tx/designations.test.js @@ -123,7 +123,7 @@ describe('Designations', () => { test('should find preferred designation without language list', () => { const preferred = designations.preferredDesignation(); - expect(preferred.display).toBe('Base English'); + expect(preferred.display).toBe('US English Display'); expect(designations.isDisplay(preferred)).toBe(true); }); diff --git a/tests/tx/expand.test.js b/tests/tx/expand.test.js index b956a07..f1f5596 100644 --- a/tests/tx/expand.test.js +++ b/tests/tx/expand.test.js @@ -373,7 +373,7 @@ describe('Expand Worker', () => { // Currently returns 200 with empty expansion (stub behavior) // When doExpand is implemented, this should return an error // because the CodeSystem can't be found - expect(response.status).toBe(404); + expect(response.status).toBe(422); // expect(response.status).toBe(404); // or 400 when fully implemented }); }); diff --git a/tests/tx/lookup.test.js b/tests/tx/lookup.test.js index 4685320..987a1d6 100644 --- a/tests/tx/lookup.test.js +++ b/tests/tx/lookup.test.js @@ -98,7 +98,7 @@ describe('Lookup Worker', () => { expect(response.body.issue[0].code).toBe('not-found'); }); - test('should return 404 when system not found', async () => { + test('should return 422 when system not found', async () => { const response = await request(app) .get('/tx/r5/CodeSystem/$lookup') .query({ @@ -107,7 +107,7 @@ describe('Lookup Worker', () => { }) .set('Accept', 'application/json'); - expect(response.status).toBe(404); + expect(response.status).toBe(422); expect(response.body.resourceType).toBe('OperationOutcome'); }); diff --git a/tests/tx/subsumes.test.js b/tests/tx/subsumes.test.js index 3048854..024b188 100644 --- a/tests/tx/subsumes.test.js +++ b/tests/tx/subsumes.test.js @@ -78,7 +78,7 @@ describe('Subsumes Worker', () => { expect(response.body.resourceType).toBe('OperationOutcome'); }); - test('should return 404 when system not found', async () => { + test('should return 422 when system not found', async () => { const response = await request(app) .get('/tx/r5/CodeSystem/$subsumes') .query({ @@ -88,7 +88,7 @@ describe('Subsumes Worker', () => { }) .set('Accept', 'application/json'); - expect(response.status).toBe(404); + expect(response.status).toBe(422); expect(response.body.resourceType).toBe('OperationOutcome'); }); diff --git a/tx/cs/cs-country.js b/tx/cs/cs-country.js index 2316042..2f690a0 100644 --- a/tx/cs/cs-country.js +++ b/tx/cs/cs-country.js @@ -578,6 +578,52 @@ class CountryCodeFactoryProvider extends CodeSystemFactoryProvider { ['ZM', 'Zambia'], ['ZW', 'Zimbabwe'], + // ISO 3166-1 User-assigned code elements + // These codes are reserved for user assignment and will never be used for country names + // See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#User-assigned_code_elements + ['AA', 'User-assigned'], + ['QM', 'User-assigned'], + ['QN', 'User-assigned'], + ['QO', 'User-assigned'], + ['QP', 'User-assigned'], + ['QQ', 'User-assigned'], + ['QR', 'User-assigned'], + ['QS', 'User-assigned'], + ['QT', 'User-assigned'], + ['QU', 'User-assigned'], + ['QV', 'User-assigned'], + ['QW', 'User-assigned'], + ['QX', 'User-assigned'], + ['QY', 'User-assigned'], + ['QZ', 'User-assigned'], + ['XA', 'User-assigned'], + ['XB', 'User-assigned'], + ['XC', 'User-assigned'], + ['XD', 'User-assigned'], + ['XE', 'User-assigned'], + ['XF', 'User-assigned'], + ['XG', 'User-assigned'], + ['XH', 'User-assigned'], + ['XI', 'User-assigned'], + ['XJ', 'User-assigned'], + ['XK', 'Kosovo'], + ['XL', 'User-assigned'], + ['XM', 'User-assigned'], + ['XN', 'User-assigned'], + ['XO', 'User-assigned'], + ['XP', 'User-assigned'], + ['XQ', 'User-assigned'], + ['XR', 'User-assigned'], + ['XS', 'User-assigned'], + ['XT', 'User-assigned'], + ['XU', 'User-assigned'], + ['XV', 'User-assigned'], + ['XW', 'User-assigned'], + ['XX', 'Unknown'], + ['XY', 'User-assigned'], + ['XZ', 'International Waters'], + ['ZZ', 'Unknown or Invalid Territory'], + // 3-letter codes ['ABW', 'Aruba'], ['AFG', 'Afghanistan'], diff --git a/tx/cs/cs-cpt.js b/tx/cs/cs-cpt.js index 50bdd81..b480230 100644 --- a/tx/cs/cs-cpt.js +++ b/tx/cs/cs-cpt.js @@ -224,6 +224,9 @@ class CPTServices extends CodeSystemProvider { } + isNotClosed() { + return true; + } async extendLookup(ctxt, props, params) { validateArrayParameter(props, 'props', String); validateArrayParameter(params, 'params', Object); @@ -545,7 +548,7 @@ class CPTServices extends CodeSystemProvider { if (concept) { return concept; } - return `Code ${code} is not in the specified filter`; + return null; } async filterCheck(filterContext, set, concept) { diff --git a/tx/cs/cs-ndc.js b/tx/cs/cs-ndc.js index ad26f1d..49c76bd 100644 --- a/tx/cs/cs-ndc.js +++ b/tx/cs/cs-ndc.js @@ -109,6 +109,12 @@ class NdcServices extends CodeSystemProvider { return ctxt ? !ctxt.active : false; } + async getStatus(code) { + + const ctxt = await this.#ensureContext(code); + return ctxt.active ? "active" : "inactive"; + } + async isDeprecated(code) { await this.#ensureContext(code); return false; // NDC doesn't track deprecated status separately diff --git a/tx/cs/cs-rxnorm.js b/tx/cs/cs-rxnorm.js index 9592939..84bdadd 100644 --- a/tx/cs/cs-rxnorm.js +++ b/tx/cs/cs-rxnorm.js @@ -527,7 +527,7 @@ class RxNormServices extends CodeSystemProvider { if (err) { reject(err); } else if (!row) { - resolve(`Code ${code} is not in the specified filter`); + resolve(null); } else { const concept = new RxNormConcept(row[this.getCodeField()], row.STR); resolve(concept); diff --git a/tx/cs/cs-snomed.js b/tx/cs/cs-snomed.js index baf7d90..79774e0 100644 --- a/tx/cs/cs-snomed.js +++ b/tx/cs/cs-snomed.js @@ -626,7 +626,7 @@ class SnomedProvider extends CodeSystemProvider { } catch (error) { return { context: null, - message: `Code ${code} is not a valid SNOMED CT Term, and could not be parsed as an expression (${error.message})` + message: `Code ${code} is not a valid SNOMED CT Term, and neither could it be parsed as an expression (${error.message})` }; } } else { @@ -848,7 +848,7 @@ class SnomedProvider extends CodeSystemProvider { if (found) { return ctxt; } else { - return `Code ${code} is not in the specified filter`; + return null; } } diff --git a/tx/cs/cs-ucum.js b/tx/cs/cs-ucum.js index e6dd24f..a7829d9 100644 --- a/tx/cs/cs-ucum.js +++ b/tx/cs/cs-ucum.js @@ -9,6 +9,7 @@ const ValueSet = require("../library/valueset"); const assert = require('assert'); const {UcumService} = require("../library/ucum-service"); const {validateArrayParameter} = require("../../library/utilities"); +const {DesignationUse} = require("../library/designations"); /** * UCUM provider context for concepts @@ -153,8 +154,8 @@ class UcumCodeSystemProvider extends CodeSystemProvider { return supplementDisplay; } - // Fall back to analysis - return this.ucumService.analyse(ctxt.code); + // Fall back to code per THO advice + return ctxt.code; } async definition(code) { @@ -187,15 +188,16 @@ class UcumCodeSystemProvider extends CodeSystemProvider { // Primary display (analysis) const analysis = this.ucumService.analyse(ctxt.code); - displays.addDesignation(true, 'active', 'en', CodeSystem.makeUseForDisplay(), analysis); + displays.addDesignation(true, 'active', 'en', DesignationUse.DISPLAY, ctxt.code); + displays.addDesignation(true, 'active', 'en', DesignationUse.SYNONYM, analysis); // Common unit display if available if (this.commonUnitList) { for (const concept of this.commonUnitList) { if (concept.code === ctxt.code && concept.display) { const display = concept.display.trim(); - if (display !== analysis) { - displays.addDesignation(false, 'active', 'en', CodeSystem.makeUseForDisplay(), display); + if (display !== ctxt.code) { + displays.addDesignation(false, 'active', 'en', DesignationUse.PREFERRED, display); } } } @@ -448,6 +450,10 @@ class UcumCodeSystemProvider extends CodeSystemProvider { isNotClosed() { return true; } + + makeUseForSynonym() { + return ; + } } /** diff --git a/tx/library.js b/tx/library.js index 8611aa4..e6328a0 100644 --- a/tx/library.js +++ b/tx/library.js @@ -442,6 +442,17 @@ class Library { // Ensure folder exists await this.ensureFolderExists(this.cacheFolder); + if (fileName.includes("|")) { + // in this case, we split it into two. if the first file exists, we go with that. Otherwise + // fallback to the second. + let firstName = fileName.substring(0, fileName.indexOf("|")); + fileName = fileName.substring(fileName.indexOf("|")+1); + + const firstPath = path.join(this.cacheFolder, firstName); + if (await this.fileExists(firstPath)) { + return firstPath; + } + } const filePath = path.join(this.cacheFolder, fileName); // Check if file already exists diff --git a/tx/library/designations.js b/tx/library/designations.js index 0250668..4bd5d24 100644 --- a/tx/library/designations.js +++ b/tx/library/designations.js @@ -562,6 +562,16 @@ class Designations { if (!langList || langList.length == 0) { // No language list, prefer base designations + for (const cd of this.designations) { + if (this._isPreferred(cd) && cd.isActive()) { + return cd; + } + } + for (const cd of this.designations) { + if (this.isDisplay(cd) && cd.isActive()) { + return cd; + } + } for (const cd of this.designations) { if (this.isDisplay(cd)) { return cd; @@ -717,7 +727,10 @@ class Designations { (cd.use.system === DesignationUse.DISPLAY.system && cd.use.code === DesignationUse.DISPLAY.code) || (cd.use.system === DesignationUse.PREFERRED.system && - cd.use.code === DesignationUse.PREFERRED.code); + cd.use.code === DesignationUse.PREFERRED.code) || + // snomed + (cd.use.system === 'http://snomed.info/sct' && + cd.use.code === '900000000000013009'); } /** diff --git a/tx/provider.js b/tx/provider.js index 54409d1..866fa5b 100644 --- a/tx/provider.js +++ b/tx/provider.js @@ -135,6 +135,7 @@ class Provider { const resources = await contentLoader.getResourcesByType("CodeSystem"); for (const resource of resources) { const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion())); + cs.sourcePackage = contentLoader.pid(); this.codeSystems.set(cs.url, cs); this.codeSystems.set(cs.vurl, cs); } @@ -224,7 +225,7 @@ class Provider { async listCodeSystemVersions(url) { let result = new Set(); for (let cs of this.codeSystems.values()) { - if (cs.url == url) { + if (cs.url == url && cs.version) { result.add(cs.version); } } @@ -363,16 +364,16 @@ class Provider { factory = this.codeSystemFactories.get(vurlMM); } if (factory != null) { - const csp = factory.build(opContext, []); - const c = csp.locate(code); + const csp = await factory.build(opContext, []); + const c = csp ? csp.locate(code) : null; if (c) { - if (factory.iterable()) { + if (factory.iteratable()) { return { link: this.path + "/CodeSystem/x-" + factory.id(), description: csp.display(c) } } else { - const link = csp.codeLink(c); + const link = factory.codeLink(c); if (link) { return { link: link, diff --git a/tx/sct/expressions.js b/tx/sct/expressions.js index b7c5910..ebd424c 100644 --- a/tx/sct/expressions.js +++ b/tx/sct/expressions.js @@ -770,7 +770,7 @@ class SnomedExpressionParser { */ rule(test, message) { if (!test) { - throw new Error(message + ' at character ' + this.cursor); + throw new Error(message + ' at character ' + (this.cursor+1)); } } diff --git a/tx/tx.fhir.org.yml b/tx/tx.fhir.org.yml index fa69b46..73b12c2 100644 --- a/tx/tx.fhir.org.yml +++ b/tx/tx.fhir.org.yml @@ -24,10 +24,10 @@ sources: - snomed:sct_ips_20241216.cache - snomed:sct_nl_20240930.cache - snomed:sct_uk_20230412.cache - - snomed:sct_us_20250901.cache - snomed:sct_us_20230301.cache + - snomed:sct_us_20250901.cache - snomed:sct_test_20250814.cache - - cpt:cpt-2023-fragment-0.1.db + - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db - omop:omop_v20250227.db - npm:hl7.terminology - npm:fhir.tx.support.r4 diff --git a/tx/tx.js b/tx/tx.js index 58e8dff..50d38d6 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -36,6 +36,7 @@ const {ConceptMapXML} = require("./xml/conceptmap-xml"); const {TxHtmlRenderer} = require("./tx-html"); const {Renderer} = require("./library/renderer"); const {OperationsWorker} = require("./workers/operations"); +const {RelatedWorker} = require("./workers/related"); // const {writeFileSync} = require("fs"); class TXModule { @@ -492,6 +493,26 @@ class TXModule { } }); + // ValueSet/$related(GET and POST) + router.get('/ValueSet/\\$related', async (req, res) => { + const start = Date.now(); + try { + let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + await worker.handle(req, res); + } finally { + this.countRequest('$related', Date.now() - start); + } + }); + router.post('/ValueSet/\\$related', async (req, res) => { + const start = Date.now(); + try { + let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + await worker.handle(req, res); + } finally { + this.countRequest('$related', Date.now() - start); + } + }); + // ValueSet/$batch-validate-code (GET and POST) router.get('/ValueSet/\\$batch-validate-code', async (req, res) => { const start = Date.now(); @@ -655,6 +676,27 @@ class TXModule { } }); + + // ValueSet/[id]/$related + router.get('/ValueSet/:id/\\$related', async (req, res) => { + const start = Date.now(); + try { + let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + await worker.handleInstance(req, res, this.log); + } finally { + this.countRequest('$related', Date.now() - start); + } + }); + router.post('/ValueSet/:id/\\$related', async (req, res) => { + const start = Date.now(); + try { + let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n); + await worker.handleInstance(req, res, this.log); + } finally { + this.countRequest('$related', Date.now() - start); + } + }); + // ValueSet/[id]/$expand router.get('/ValueSet/:id/\\$expand', async (req, res) => { const start = Date.now(); diff --git a/tx/workers/expand.js b/tx/workers/expand.js index dee4f84..0756944 100644 --- a/tx/workers/expand.js +++ b/tx/workers/expand.js @@ -628,7 +628,7 @@ class ValueSetExpander { } else if (this.params.incompleteOK) { this.addParamUri(cs.contentMode, cs.system + '|' + cs.version); } else { - throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' is a ' + cs.contentMode + ', so this expansion is not permitted unless the expansion parameter "incomplete-ok" has a value of "true"', 'invalid'); + throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' is a ' + cs.contentMode() + ', so this expansion is not permitted unless the expansion parameter "incomplete-ok" has a value of "true"', 'invalid', 422); } } diff --git a/tx/workers/related.js b/tx/workers/related.js index 5fcc646..6a40c03 100644 --- a/tx/workers/related.js +++ b/tx/workers/related.js @@ -1,1522 +1,21 @@ // -// Expand Worker - Handles ValueSet $expand operation +// Related Worker - Handles ValueSet $related operation // -// GET /ValueSet/{id}/$expand -// GET /ValueSet/$expand?url=...&version=... -// POST /ValueSet/$expand (form body or Parameters with url) -// POST /ValueSet/$expand (body is ValueSet resource) -// POST /ValueSet/$expand (body is Parameters with valueSet parameter) +// GET /ValueSet/{id}/$related +// GET /ValueSet/$related?url=...&version=... +// POST /ValueSet/$related (form body or Parameters with url) +// POST /ValueSet/$related (body is ValueSet resource) +// POST /ValueSet/$related (body is Parameters with valueSet parameter) // const { TerminologyWorker } = require('./worker'); const {TxParameters} = require("../params"); -const {Designations, SearchFilterText} = require("../library/designations"); const {Extensions} = require("../library/extensions"); -const {getValuePrimitive, getValueName} = require("../../library/utilities"); -const {div} = require("../../library/html"); const {Issue, OperationOutcome} = require("../library/operation-outcome"); -const crypto = require('crypto'); const ValueSet = require("../library/valueset"); -const {VersionUtilities} = require("../../library/version-utilities"); -// Expansion limits (from Pascal constants) -const UPPER_LIMIT_NO_TEXT = 1000; -const UPPER_LIMIT_TEXT = 1000; -const INTERNAL_LIMIT = 10000; -const EXPANSION_DEAD_TIME_SECS = 30; -const CACHE_WHEN_DEBUGGING = false; -/** - * Total status for expansion - */ -const TotalStatus = { - Uninitialized: 'uninitialized', - Set: 'set', - Off: 'off' -}; - -/** - * Wraps an already-expanded ValueSet for fast code lookups - * Used when importing ValueSets during expansion - */ -class ImportedValueSet { - /** - * @param {Object} valueSet - Expanded ValueSet resource - */ - constructor(valueSet) { - this.valueSet = valueSet; - this.url = valueSet.url || ''; - this.version = valueSet.version || ''; - - /** @type {Map} Maps system|code -> contains entry */ - this.codeMap = new Map(); - - /** @type {Set} Set of systems in this ValueSet */ - this.systems = new Set(); - - this._buildCodeMap(); - } - - /** - * Build the code lookup map from the expansion - * @private - */ - _buildCodeMap() { - if (!this.valueSet.expansion || !this.valueSet.expansion.contains) { - return; - } - - this._indexContains(this.valueSet.expansion.contains); - } - - /** - * Recursively index contains entries - * @private - */ - _indexContains(contains) { - for (const entry of contains) { - if (entry.system && entry.code) { - const key = this._makeKey(entry.system, entry.code); - this.codeMap.set(key, entry); - this.systems.add(entry.system); - } - - // Handle nested contains (hierarchy) - if (entry.contains && entry.contains.length > 0) { - this._indexContains(entry.contains); - } - } - } - - /** - * Make a lookup key from system and code - * @private - */ - _makeKey(system, code) { - return `${system}\x00${code}`; - } - - /** - * Check if this ValueSet contains a specific code - * @param {string} system - Code system URL - * @param {string} code - Code value - * @returns {boolean} - */ - hasCode(system, code) { - return this.codeMap.has(this._makeKey(system, code)); - } - - /** - * Get a contains entry for a specific code - * @param {string} system - Code system URL - * @param {string} code - Code value - * @returns {Object|null} - */ - getCode(system, code) { - return this.codeMap.get(this._makeKey(system, code)) || null; - } - - /** - * Check if this ValueSet contains any codes from a system - * @param {string} system - Code system URL - * @returns {boolean} - */ - hasSystem(system) { - return this.systems.has(system); - } - - /** - * Get total number of codes - * @returns {number} - */ - get count() { - return this.codeMap.size; - } - - /** - * Iterate over all codes - * @yields {{system: string, code: string, entry: Object}} - */ - *codes() { - for (const entry of this.codeMap.values()) { - yield { - system: entry.system, - code: entry.code, - entry - }; - } - } -} - -/** - * Special filter context for ValueSet import optimization - * When a ValueSet can be used as a filter instead of full expansion - */ -class ValueSetFilterContext { - /** - * @param {ImportedValueSet} importedVs - The imported ValueSet - */ - constructor(importedVs) { - this.importedVs = importedVs; - this.type = 'valueset'; - } - - /** - * Check if a code passes this filter - * @param {string} system - Code system URL - * @param {string} code - Code value - * @returns {boolean} - */ - passesFilter(system, code) { - return this.importedVs.hasCode(system, code); - } -} - -/** - * Special filter context for empty filter (nothing matches) - */ -class EmptyFilterContext { - constructor() { - this.type = 'empty'; - } - - passesFilter() { - return false; - } -} - -class ValueSetCounter { - constructor() { - this.count = 0; - } - - increment() { - this.count++; - } -} - -class ValueSetExpander { - worker; - params; - excluded = new Set(); - hasExclusions = false; - - constructor(worker, params) { - this.worker = worker; - this.params = params; - - this.csCounter = new Map(); - } - - addDefinedCode(cs, system, c, imports, parent, excludeInactive, srcURL) { - this.worker.deadCheck('addDefinedCode'); - let n = null; - if (!this.params.excludeNotForUI || !cs.isAbstract(c)) { - const cds = new Designations(this.worker.opContext.i18n.languageDefinitions); - this.listDisplays(cds, c); - n = this.includeCode(null, parent, system, '', c.code, cs.isAbstract(c), cs.isInactive(c), cs.isDeprecated(c), cs.codeStatus(c), cds, c.definition, c.itemWeight, - null, imports, c.getAllExtensionsW(), null, c.properties, null, excludeInactive, srcURL); - } - for (let i = 0; i < c.concept.length; i++) { - this.worker.deadCheck('addDefinedCode'); - this.addDefinedCode(cs, system, c.concept[i], imports, n, excludeInactive, srcURL); - } - } - - async listDisplaysFromProvider(displays, cs, context) { - await cs.designations(context, displays); - displays.source = cs; - } - - listDisplaysFromConcept(displays, concept) { - for (const ccd of concept.designations || []) { - displays.addDesignation(ccd); - } - } - - listDisplaysFromIncludeConcept(displays, concept, vs) { - if (concept.display) { - if (!VersionUtilities.isR4Plus(this.worker.provider.getFhirVersion())) { - displays.clear(); - } - let lang = vs.language ? this.worker.languages.parse(vs.language) : null; - displays.addDesignation(true, "active", lang, null, concept.display); - } - for (const cd of concept.designation || []) { - displays.addDesignationFromConcept(cd); - } - } - canonical(system, version) { - if (!version) { - return system; - } else { - return system + '|' + version; - } - } - - passesImport(imp, system, code) { - imp.buildMap(); - return imp.hasCode(system, code); - } - - passesImports(imports, system, code, offset) { - if (imports == null) { - return true; - } - for (let i = offset; i < imports.length; i++) { - if (!this.passesImport(imports[i], system, code)) { - return false; - } - } - return true; - } - - useDesignation(cd) { - if (!this.params.hasDesignations) { - return true; - } - for (const s of this.params.designations) { - const [l, r] = s.split('|'); - if (cd.use != null && cd.use.system === l && cd.use.code === r) { - return true; - } - if (cd.language != null && l === 'urn:ietf:bcp:47' && r === cd.language.code) { - return true; - } - } - return false; - } - - isValidating() { - return false; - } - - opName() { - return 'expansion'; - } - - redundantDisplay(n, lang, use, value) { - if (!((lang == null) && (!this.valueSet.language)) || ((lang) && lang.code.startsWith(this.valueSet.language))) { - return false; - } else if (!((use == null) || (use.code === 'display'))) { - return false; - } else { - return value.asString === n.display; - } - } - - includeCode(cs, parent, system, version, code, isAbstract, isInactive, deprecated, status, displays, definition, itemWeight, expansion, imports, csExtList, vsExtList, csProps, expProps, excludeInactive, srcURL) { - let result = null; - this.worker.deadCheck('processCode'); - - if (!this.passesImports(imports, system, code, 0)) { - return null; - } - if (isInactive && excludeInactive) { - return null; - } - if (this.isExcluded(system, version, code)) { - return null; - } - - if (cs != null && cs.expandLimitation > 0) { - let cnt = this.csCounter.get(cs.system); - if (cnt == null) { - cnt = new ValueSetCounter(); - this.csCounter.set(cs.system, cnt); - } - cnt.increment(); - if (cnt.count > cs.expandLimitation) { - return null; - } - } - - if (this.limitCount > 0 && this.fullList.length >= this.limitCount && !this.hasExclusions) { - if (this.count > -1 && this.offset > -1 && this.count + this.offset > 0 && this.fullList.length >= this.count + this.offset) { - throw new Issue('information', 'informational', null, null, null, null).setFinished(); - } else { - if (!srcURL) { - srcURL = '??'; - } - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [srcURL, '>' + this.limitCount]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - if (expansion) { - const s = this.canonical(system, version); - this.addParamUri(expansion, 'used-codesystem', s); - if (cs != null) { - const ts = cs.listSupplements(); - for (const vs of ts) { - this.addParamUri(expansion, 'used-supplement', vs); - } - } - } - - const s = this.keyS(system, version, code); - - if (!this.map.has(s)) { - const n = {}; - n.system = system; - n.code = code; - if (this.doingVersion) { - n.version = version; - } - if (isAbstract) { - n.abstract = isAbstract; - } - if (isInactive) { - n.inactive = true; - } - - if (status && status.toLowerCase() !== 'active') { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#status', 'status', "valueCode", status); - } else if (deprecated) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#status', 'status', "valueCode", 'deprecated'); - } - - if (Extensions.has(csExtList, 'http://hl7.org/fhir/StructureDefinition/codesystem-label')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#label', 'label', "valueString", Extensions.readString(csExtList, 'http://hl7.org/fhir/StructureDefinition/codesystem-label')); - } - if (Extensions.has(vsExtList, 'http://hl7.org/fhir/StructureDefinition/valueset-label')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#label', 'label', "valueString", Extensions.readString(vsExtList, 'http://hl7.org/fhir/StructureDefinition/valueset-label')); - } - - if (Extensions.has(csExtList, 'http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#order', 'order', "valueDecimal", Extensions.readNumber(csExtList, 'http://hl7.org/fhir/StructureDefinition/codesystem-conceptOrder', undefined)); - } - if (Extensions.has(vsExtList, 'http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#order', 'order', "valueDecimal", Extensions.readNumber(vsExtList, 'http://hl7.org/fhir/StructureDefinition/valueset-conceptOrder', undefined)); - } - - if (Extensions.has(csExtList, 'http://hl7.org/fhir/StructureDefinition/itemWeight')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#itemWeight', 'weight', "valueDecimal", Extensions.readNumber(csExtList, 'http://hl7.org/fhir/StructureDefinition/itemWeight', undefined)); - } - if (Extensions.has(vsExtList, 'http://hl7.org/fhir/StructureDefinition/itemWeight')) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#itemWeight', 'weight', "valueDecimal", Extensions.readNumber(vsExtList, 'http://hl7.org/fhir/StructureDefinition/itemWeight', undefined)); - } - - if (csExtList != null) { - for (const ext of csExtList) { - if (['http://hl7.org/fhir/StructureDefinition/coding-sctdescid', 'http://hl7.org/fhir/StructureDefinition/rendering-style', - 'http://hl7.org/fhir/StructureDefinition/rendering-xhtml', 'http://hl7.org/fhir/StructureDefinition/codesystem-alternate'].includes(ext.url)) { - if (!n.extension) {n.extension = []} - n.extension.push(ext); - } - if (['http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'].includes(ext.url)) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#status', 'status', "valueCode", getValuePrimitive(ext)); - } - } - } - - if (vsExtList != null) { - for (const ext of vsExtList || []) { - if (['http://hl7.org/fhir/StructureDefinition/valueset-supplement', 'http://hl7.org/fhir/StructureDefinition/valueset-deprecated', - 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status', - 'http://hl7.org/fhir/StructureDefinition/valueset-concept-definition', 'http://hl7.org/fhir/StructureDefinition/coding-sctdescid', - 'http://hl7.org/fhir/StructureDefinition/rendering-style', 'http://hl7.org/fhir/StructureDefinition/rendering-xhtml'].includes(ext.url)) { - if (!n.extension) {n.extension = []} - n.extension.push(ext); - } - } - } - - // display and designations - const pref = displays.preferredDesignation(this.params.workingLanguages()); - if (pref && pref.value) { - n.display = pref.value; - } - - if (this.params.includeDesignations) { - for (const t of displays.designations) { - if (t !== pref && this.useDesignation(t) && t.value != null && !this.redundantDisplay(n, t.language, t.use, t.value)) { - if (!n.designation) { - n.designation = []; - } - n.designation.push(t.asObject()); - } - } - } - - for (const pn of this.params.properties) { - if (pn === 'definition') { - if (definition) { - this.defineProperty(expansion, n, 'http://hl7.org/fhir/concept-properties#definition', pn, "valueString", definition); - } - } else if (csProps != null && cs != null) { - for (const cp of csProps) { - if (cp.code === pn) { - let vn = getValueName(cp); - let v = cp[vn]; - this.defineProperty(expansion, n, this.getPropUrl(cs, pn), pn, vn, v); - } - } - } - } - - if (!this.map.has(s)) { - this.fullList.push(n); - this.map.set(s, n); - if (parent != null) { - if (!parent.contains) { - parent.contains = []; - } - parent.contains.push(n); - } else { - this.rootList.push(n); - } - } else { - this.canBeHierarchy = false; - } - result = n; - } - return result; - } - - excludeCode(cs, system, version, code, expansion, imports, srcURL) { - this.worker.deadCheck('excludeCode'); - if (!this.passesImports(imports, system, code, 0)) { - return; - } - - if (this.limitCount > 0 && this.fullList.length >= this.limitCount && !this.hasExclusions) { - if (this.count > -1 && this.offset > -1 && this.count + this.offset > 0 && this.fullList.length >= this.count + this.offset) { - throw new Issue('information', 'informational', null, null, null, null).setFinished(); - } else { - if (!srcURL) { - srcURL = '??'; - } - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [srcURL, '>' + this.limitCount]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - if (expansion) { - const s = this.canonical(system, version); - this.addParamUri(expansion, 'used-codesystem', s); - if (cs) { - const ts= cs.listSupplements(); - for (const vs of ts) { - this.addParamUri(expansion, 'used-supplement', vs); - } - } - } - - this.excluded.add(system + '|' + version + '#' + code); - } - - async checkCanExpandValueset(uri, version) { - const vs = await this.worker.findValueSet(uri, version); - if (vs == null) { - if (!version && uri.includes('|')) { - version = uri.substring(uri.indexOf('|') + 1); - uri = uri.substring(0, uri.indexOf('|')); - } - if (!version) { - throw new Issue('error', 'not-found', null, 'VS_EXP_IMPORT_UNK', this.worker.i18n.translate('VS_EXP_IMPORT_UNK', this.params.httpLanguages, [uri]), 'unknown', 400); - } else { - 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', 400); - } - } else { - this.worker.seeSourceVS(vs, uri); - } - } - - async expandValueSet(uri, version, filter, notClosed) { - - let vs = await this.worker.findValueSet(uri, version); - if (!vs) { - if (version) { - 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", 400); - } else if (uri.includes('|')) { - 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.substring(0, uri.indexOf("|")), uri.substring(uri.indexOf("|")+1)]), "not-found", 400); - } else { - throw new Issue('error', 'not-found', null, 'VS_EXP_IMPORT_UNK', this.worker.i18n.translate('VS_EXP_IMPORT_UNK', this.params.httpLanguages, [uri]), "not-found", 400); - } - } - let worker = new ExpandWorker(this.worker.opContext, this.worker.log, this.worker.provider, this.worker.languages, this.worker.i18n); - worker.additionalResources = this.worker.additionalResources; - let expander = new ValueSetExpander(worker, this.params); - let result = await expander.expand(vs, filter, false); - if (result == null) { - throw new Issue('error', 'not-found', null, 'VS_EXP_IMPORT_UNK', this.worker.i18n.translate('VS_EXP_IMPORT_UNK', this.params.httpLanguages, [uri]), 'unknown'); - } - if (Extensions.has(result.expansion, 'http://hl7.org/fhir/params/questionnaire-extensions#closed')) { - notClosed.value = true; - } - return result; - } - - async importValueSet(vs, expansion, imports, offset) { - this.canBeHierarchy = false; - for (let p of vs.expansion.parameter) { - let vn = getValueName(p); - let v = getValuePrimitive(p); - this.addParam(expansion, p.name, vn, v); - } - this.checkResourceCanonicalStatus(expansion, vs, this.valueSet); - - for (const c of vs.expansion.contains || []) { - this.worker.deadCheck('importValueSet'); - await this.importValueSetItem(null, c, imports, offset); - } - } - - async importValueSetItem(p, c, imports, offset) { - this.worker.deadCheck('importValueSetItem'); - const s = this.keyC(c); - if (this.passesImports(imports, c.system, c.code, offset) && !this.map.has(s)) { - this.fullList.push(c); - if (p != null) { - if (!p.contains) {p.contains = [] } - p.contains.push(c); - } else { - this.rootList.push(c); - } - this.map.set(s, c); - } - for (const cc of c.contains || []) { - this.worker.deadCheck('importValueSetItem'); - await this.importValueSetItem(c, cc, imports, offset); - } - } - - excludeValueSet(vs, expansion, imports, offset) { - for (const c of vs.expansion.contains) { - this.worker.deadCheck('excludeValueSet'); - const s = this.keyC(c); - if (this.passesImports(imports, c.system, c.code, offset) && this.map.has(s)) { - const idx = this.fullList.indexOf(this.map.get(s)); - if (idx >= 0) { - this.fullList.splice(idx, 1); - } - this.map.delete(s); - } - } - } - - async checkSource(cset, exp, filter, srcURL, ts) { - this.worker.deadCheck('checkSource'); - Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set'); - let imp = false; - for (const u of cset.valueSet || []) { - this.worker.deadCheck('checkSource'); - const s = this.worker.pinValueSet(u); - await this.checkCanExpandValueset(s, ''); - imp = true; - } - - if (ts.has(cset.system)) { - const v = ts.get(cset.system); - if (v !== cset.version) { - this.doingVersion = true; - } - } else { - ts.set(cset.system, cset.version); - } - - if (cset.system) { - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, true, true, null); - this.worker.seeSourceProvider(cs, cset.system); - if (cs == null) { - // nothing - } else { - if (cs.contentMode() !== 'complete') { - if (cs.contentMode() === 'not-present') { - 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 (this.params.incompleteOK) { - exp.addParamUri(cs.contentMode, cs.system + '|' + cs.version); - } else { - throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' is a ' + cs.contentMode + ', so this expansion is not permitted unless the expansion parameter "incomplete-ok" has a value of "true"', 'invalid'); - } - } - - if (!cset.concept && !cset.filter) { - if (cs.specialEnumeration() && this.params.limitedExpansion) { - this.checkCanExpandValueSet(cs.specialEnumeration(), ''); - } else if (filter.isNull) { - if (cs.isNotClosed()) { - if (cs.specialEnumeration()) { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly. If an incomplete expansion is requested, a limited enumeration will be returned', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } else { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - if (!imp && this.limitCount > 0 && cs.totalCount > this.limitCount && !this.params.limitedExpansion) { - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [srcURL, '>' + this.limitCount]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - } - } - } - } - - async includeCodes(cset, path, vsSrc, filter, expansion, excludeInactive, notClosed) { - this.worker.deadCheck('processCodes#1'); - const valueSets = []; - - Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set'); - - if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) { - this.canBeHierarchy = false; - } - - if (!cset.system) { - for (const u of cset.valueSet) { - this.worker.deadCheck('processCodes#2'); - const s = this.worker.pinValueSet(u); - this.worker.opContext.log('import value set ' + s); - const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed)); - this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet); - this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet)); - valueSets.push(ivs); - } - await this.importValueSet(valueSets[0].valueSet, expansion, valueSets, 1); - } else { - const filters = []; - const prep = null; - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, false, true, null); - - if (cs == null) { - // nothing - } else { - - this.worker.checkSupplements(cs, cset, this.requiredSupplements); - this.checkProviderCanonicalStatus(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.pinValueSet(u); - let f = null; - this.opContext.log('import2 value set ' + s); - const vs = this.onGetValueSet(this, s, ''); - if (vs != null) { - f = this.makeFilterForValueSet(cs, vs); - } - if (f != null) { - filters.push(f); - } else { - valueSets.push(new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed))); - } - } - - if (!cset.concept && !cset.filter) { - if (cs.specialEnumeration() && this.params.limitedExpansion && filters.length === 0) { - this.worker.opContext.log('import special value set ' + cs.specialEnumeration()); - const base = await this.expandValueSet(cs.specialEnumeration(), '', filter, notClosed); - Extensions.addBoolean(expansion, 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly', true); - await this.importValueSet(base, expansion, valueSets, 0); - notClosed.value = true; - } else if (filter.isNull) { - this.worker.opContext.log('add whole code system'); - if (cs.isNotClosed()) { - if (cs.specialEnumeration()) { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly. If an incomplete expansion is requested, a limited enumeration will be returned', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - - } else { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - const iter = await cs.iterator(null); - if (valueSets.length === 0 && this.limitCount > 0 && (iter && iter.total > this.limitCount) && !this.params.limitedExpansion && this.offset < 0) { - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [vsSrc.vurl, '>' + this.limitCount]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - - } - let tcount = 0; - let c = await cs.nextContext(iter); - while (c) { - this.worker.deadCheck('processCodes#3a'); - if (await this.passesFilters(cs, c, prep, filters, 0)) { - tcount += await this.includeCodeAndDescendants(cs, c, expansion, valueSets, null, excludeInactive, vsSrc.vurl); - } - c = await cs.nextContext(iter); - } - this.addToTotal(tcount); - } else { - this.worker.opContext.log('prepare filters'); - this.noTotal(); - if (cs.isNotClosed(filter)) { - notClosed.value = true; - } - const prep = await cs.getPrepContext(true); - const ctxt = await cs.searchFilter(filter, prep, false); - await cs.prepare(prep); - this.worker.opContext.log('iterate filters'); - while (await cs.filterMore(ctxt)) { - this.worker.deadCheck('processCodes#4'); - const c = await cs.filterConcept(ctxt); - if (await this.passesFilters(cs, c, prep, filters, 0)) { - const cds = new Designations(this.worker.i18n.languageDefinitions); - await this.listDisplaysFromProvider(cds, cs, c); - await this.includeCode(cs, null, await cs.system(), await cs.version(), await cs.code(c), await cs.isAbstract(c), await cs.isInactive(c), await cs.deprecated(c), await cs.getCodeStatus(c), - cds, await cs.definition(c), await cs.itemWeight(c), expansion, valueSets, await cs.getExtensions(c), null, await cs.getProperties(c), null, excludeInactive, vsSrc.url); - } - } - this.worker.opContext.log('iterate filters done'); - } - } - - if (cset.concept) { - this.worker.opContext.log('iterate concepts'); - const cds = new Designations(this.worker.i18n.languageDefinitions); - let tcount = 0; - for (const cc of cset.concept) { - this.worker.deadCheck('processCodes#3'); - cds.clear(); - Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference'); - 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)) { - await this.listDisplaysFromProvider(cds, cs, cctxt.context); - this.listDisplaysFromIncludeConcept(cds, cc, vsSrc); - if (filter.passesDesignations(cds) || filter.passes(cc.code)) { - tcount++; - let ov = Extensions.readString(cc, 'http://hl7.org/fhir/StructureDefinition/itemWeight'); - if (!ov) { - ov = await cs.itemWeight(cctxt.context); - } - await this.includeCode(cs, null, cs.system(), cs.version(), cc.code, await cs.isAbstract(cctxt.context), await cs.isInactive(cctxt.context), await cs.isDeprecated(cctxt.context), await cs.getStatus(cctxt.context), cds, - await cs.definition(cctxt.context), ov, expansion, valueSets, await cs.extensions(cctxt.context), cc.extension, await cs.properties(cctxt.context), null, excludeInactive, vsSrc.url); - } - } - } - this.addToTotal(tcount); - this.worker.opContext.log('iterate concepts done'); - } - - if (cset.filter) { - this.worker.opContext.log('prepare filters'); - const fcl = cset.filter; - 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-toocostly', true); - notClosed.value = true; - } - - for (let i = 0; i < fcl.length; i++) { - this.worker.deadCheck('processCodes#4a'); - const fc = fcl[i]; - if (!fc.value) { - 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); - } - Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter'); - await cs.filter(prep, fc.property, fc.op, fc.value); - } - - const fset = await cs.executeFilters(prep); - if (await cs.filtersNotClosed(prep)) { - notClosed.value = true; - } - if (fset.length === 1 && !excludeInactive && !this.params.activeOnly) { - this.addToTotal(await cs.filterSize(prep, fset[0])); - } - - // let count = 0; - this.worker.opContext.log('iterate filters'); - 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++; - const cds = new Designations(this.worker.i18n.languageDefinitions); - if (this.passesImports(valueSets, cs.system(), await cs.code(c), 0)) { - await this.listDisplaysFromProvider(cds, cs, c); - let parent = null; - if (cs.hasParents()) { - parent = this.map.get(this.keyS(cs.system(), cs.version(), await cs.parent(c))); - } else { - this.canBeHierarchy = false; - } - await this.includeCode(cs, parent, await cs.system(), await cs.version(), await cs.code(c), await cs.isAbstract(c), await cs.isInactive(c), - await cs.isDeprecated(c), await cs.getStatus(c), cds, await cs.definition(c), await cs.itemWeight(c), - expansion, null, await cs.extensions(c), null, await cs.properties(c), null, excludeInactive, vsSrc.url); - - } - } - } - this.worker.opContext.log('iterate filters done'); - } - - } - } - } - - async passesFilters(cs, c, prep, filters, offset) { - for (let j = offset; j < filters.length; j++) { - const f = filters[j]; - // if (f instanceof SpecialProviderFilterContextNothing) { - // return false; - // } else if (f instanceof SpecialProviderFilterContextConcepts) { - // let ok = false; - // for (const t of f.list) { - // if (cs.sameContext(t, c)) { - // ok = true; - // } - // } - // if (!ok) return false; - // } else { - let ok = await cs.filterCheck(prep, f, c); - if (ok != true) { - return false; - } - // } - } - return true; - } - - async excludeCodes(cset, path, vsSrc, filter, expansion, excludeInactive, notClosed) { - this.worker.deadCheck('processCodes#1'); - const valueSets = []; - - Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set'); - - if (cset.valueSet || cset.concept || (cset.filter || []).length > 1) { - this.canBeHierarchy = false; - } - - if (!cset.system) { - if (cset.valueSet) { - this.noTotal(); - for (const u of cset.valueSet) { - const s = this.worker.pinValueSet(u); - this.worker.deadCheck('processCodes#2'); - const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed)); - this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet); - this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl); - valueSets.push(ivs); - } - this.excludeValueSet(valueSets[0].valueSet, expansion, valueSets, 1); - } - } else { - const filters = []; - const prep = null; - const cs = await this.worker.findCodeSystem(cset.system, cset.version, this.params, ['complete', 'fragment'], false, true, true, null); - - this.worker.checkSupplements(cs, cset, this.requiredSupplements); - 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 || []) { - const s = this.pinValueSet(u); - this.worker.deadCheck('processCodes#3'); - let f = null; - const vs = this.onGetValueSet(this, s, ''); - if (vs != null) { - f = this.makeFilterForValueSet(cs, vs); - } - if (f != null) { - filters.push(f); - } else { - valueSets.push(new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed))); - } - } - - if (!cset.concept && !cset.filter) { - this.opContext.log('handle system'); - if (cs.specialEnumeration() && this.params.limitedExpansion && filters.length === 0) { - const base = await this.expandValueSet(cs.specialEnumeration(), '', filter, notClosed); - Extensions.addBoolean(expansion, 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly', true); - this.excludeValueSet(base, expansion, valueSets, 0); - notClosed.value = true; - } else if (filter.isNull) { - if (cs.isNotClosed(filter)) { - if (cs.specialEnumeration()) { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly. If an incomplete expansion is requested, a limited enumeration will be returned', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } else { - throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system + '" has a grammar, and cannot be enumerated directly', null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - } - - const iter = await cs.getIterator(null); - if (valueSets.length === 0 && this.limitCount > 0 && iter.count > this.limitCount && !this.params.limitedExpansion) { - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [vsSrc.url, '>' + this.limitCount]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } - while (iter.more()) { - this.worker.deadCheck('processCodes#3a'); - const c = await cs.getNextContext(iter); - if (await this.passesFilters(cs, c, prep, filters, 0)) { - await this.excludeCodeAndDescendants(cs, c, expansion, valueSets, excludeInactive, vsSrc.url); - } - } - } else { - this.noTotal(); - if (cs.isNotClosed(filter)) { - notClosed.value = true; - } - const prep = await cs.getPrepContext(true); - const ctxt = await cs.searchFilter(filter, prep, false); - await cs.prepare(prep); - while (await cs.filterMore(ctxt)) { - this.worker.deadCheck('processCodes#4'); - const c = await cs.filterConcept(ctxt); - if (await this.passesFilters(cs, c, prep, filters, 0)) { - this.excludeCode(cs, await cs.system(), await cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url); - } - } - } - } - - 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'); - const cctxt = await cs.locate(cc.code, this.allAltCodes); - if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt)) && await this.passesFilters(cs, cctxt, 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); - } - } - } - } - - 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-toocostly', true); - notClosed.value = true; - } - - for (let fc of cset.filter) { - this.worker.deadCheck('processCodes#4a'); - Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter'); - await cs.filter(prep, fc.property, fc.op, fc.value); - } - - 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'); - } - } - } - - async includeCodeAndDescendants(cs, context, expansion, imports, parent, excludeInactive, srcUrl) { - let result = 0; - this.worker.deadCheck('processCodeAndDescendants'); - - if (expansion) { - const vs = this.canonical(await cs.system(), await cs.version()); - this.addParamUri(expansion, 'used-codesystem', vs); - const ts = cs.listSupplements(); - for (const v of ts) { - this.worker.deadCheck('processCodeAndDescendants'); - this.addParamUri(expansion, 'used-supplement', v); - } - } - - let n = null; - if ((!this.params.excludeNotForUI || !await cs.isAbstract(context)) && (!this.params.activeOnly || !await cs.isInactive(context))) { - const cds = new Designations(this.worker.i18n.languageDefinitions); - await this.listDisplaysFromProvider(cds, cs, context); - const t = await this.includeCode(cs, parent, await cs.system(), await cs.version(), context.code, await cs.isAbstract(context), await cs.isInactive(context), await cs.isDeprecated(context), await cs.getStatus(context), cds, await cs.definition(context), - await cs.itemWeight(context), expansion, imports, await cs.extensions(context), null, await cs.properties(context), null, excludeInactive, srcUrl); - if (t != null) { - result++; - } - if (n == null) { - n = t; - } - } else { - n = parent; - } - - const iter = await cs.iterator(context); - if (iter) { - let c = await cs.nextContext(iter); - while (c) { - this.worker.deadCheck('processCodeAndDescendants#3'); - result += await this.includeCodeAndDescendants(cs, c, expansion, imports, n, excludeInactive, srcUrl); - c = await cs.nextContext(iter); - } - } - return result; - } - - async excludeCodeAndDescendants(cs, context, expansion, imports, excludeInactive, srcUrl) { - this.worker.deadCheck('processCodeAndDescendants'); - - if (expansion) { - const vs = this.canonical(await cs.system(), await cs.version()); - this.addParamUri(expansion, 'used-codesystem', vs); - const ts= cs.listSupplements(); - for (const v of ts) { - this.worker.deadCheck('processCodeAndDescendants'); - this.addParamUri(expansion, 'used-supplement', v); - } - } - - if ((!this.params.excludeNotForUI || !await cs.isAbstract(context)) && (!this.params.activeOnly || !await cs.isInactive(context))) { - const cds = new Designations(this.worker.i18n.languageDefinitions); - await this.listDisplaysFromProvider(cds, cs, context); - for (const code of await cs.listCodes(context, this.params.altCodeRules)) { - this.worker.deadCheck('processCodeAndDescendants#2'); - this.excludeCode(cs, await cs.system(), await cs.version(), code, expansion, imports, srcUrl); - } - } - - const iter = await cs.getIterator(context); - while (iter.more()) { - this.worker.deadCheck('processCodeAndDescendants#3'); - const c = await cs.getNextContext(iter); - await this.excludeCodeAndDescendants(cs, c, expansion, imports, excludeInactive, srcUrl); - } - } - - async handleCompose(source, filter, expansion, notClosed) { - this.worker.opContext.log('compose #1'); - - const ts = new Map(); - for (const c of source.jsonObj.compose.include || []) { - this.worker.deadCheck('handleCompose#2'); - await this.checkSource(c, expansion, filter, source.url, ts); - } - for (const c of source.jsonObj.compose.exclude || []) { - this.worker.deadCheck('handleCompose#3'); - this.hasExclusions = true; - await this.checkSource(c, expansion, filter, source.url, ts); - } - - this.worker.opContext.log('compose #2'); - - let i = 0; - for (const c of source.jsonObj.compose.exclude || []) { - this.worker.deadCheck('handleCompose#4'); - await this.excludeCodes(c, "ValueSet.compose.exclude["+i+"]", source, filter, expansion, this.excludeInactives(source), notClosed); - } - - i = 0; - for (const c of source.jsonObj.compose.include || []) { - this.worker.deadCheck('handleCompose#5'); - await this.includeCodes(c, "ValueSet.compose.include["+i+"]", source, filter, expansion, this.excludeInactives(source), notClosed); - i++; - } - } - - excludeInactives(source) { - return source.jsonObj.compose && source.jsonObj.compose.inactive != undefined && !source.jsonObj.compose.inactive; - } - - async expand(source, filter, noCacheThisOne) { - this.noCacheThisOne = noCacheThisOne; - this.totalStatus = 'uninitialised'; - this.total = 0; - - Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet'); - Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet'); - this.worker.seeValueSet(source, this.params); - this.valueSet = source; - - const result = structuredClone(source.jsonObj); - result.id = ''; - let table = null; - let div_ = null; - - if (!this.params.includeDefinition) { - result.purpose = undefined; - result.compose = undefined; - result.description = undefined; - result.contactList = undefined; - result.copyright = undefined; - result.publisher = undefined; - result.extension = undefined; - result.text = undefined; - } - - this.requiredSupplements = []; - for (const ext of Extensions.list(source.jsonObj, 'http://hl7.org/fhir/StructureDefinition/valueset-supplement')) { - this.requiredSupplements.push(getValuePrimitive(ext)); - } - - if (result.expansion) { - return result; // just return the expansion - } - - if (this.params.generateNarrative) { - div_ = div(); - table = div_.table("grid"); - } else { - result.text = undefined; - } - - this.map = new Map(); - this.rootList = []; - this.fullList = []; - this.canBeHierarchy = !this.params.excludeNested; - - this.limitCount = INTERNAL_LIMIT; - if (this.params.limit <= 0) { - if (!filter.isNull) { - this.limitCount = UPPER_LIMIT_TEXT; - } else { - this.limitCount = UPPER_LIMIT_NO_TEXT; - } - } - this.offset = this.params.offset; - this.count = this.params.count; - - if (this.params.offset > 0) { - this.canBeHierarchy = false; - } - - const exp = {}; - exp.timestamp = new Date().toISOString(); - exp.identifier = 'urn:uuid:' + crypto.randomUUID(); - result.expansion = exp; - - if (!filter.isNull) { - this.addParamStr(exp, 'filter', filter.filter); - } - - if (this.params.hasLimitedExpansion) { - this.addParamBool(exp, 'limitedExpansion', this.params.limitedExpansion); - } - if (this.params.DisplayLanguages) { - this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asString(true)); - } else if (this.params.HTTPLanguages) { - this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asString(true)); - } - if (this.params.designations) { - for (const s of this.params.designations) { - this.addParamStr(exp, 'designation', s); - } - } - if (this.params.hasExcludeNested) { - this.addParamBool(exp, 'excludeNested', this.params.excludeNested); - } - if (this.params.hasActiveOnly) { - this.addParamBool(exp, 'activeOnly', this.params.activeOnly); - } - if (this.params.hasIncludeDesignations) { - this.addParamBool(exp, 'includeDesignations', this.params.includeDesignations); - } - if (this.params.hasIncludeDefinition) { - this.addParamBool(exp, 'includeDefinition', this.params.includeDefinition); - } - if (this.params.hasExcludeNotForUI) { - this.addParamBool(exp, 'excludeNotForUI', this.params.excludeNotForUI); - } - if (this.params.hasExcludePostCoordinated) { - this.addParamBool(exp, 'excludePostCoordinated', this.params.excludePostCoordinated); - } - - this.checkResourceCanonicalStatus(exp, source, source); - - if (this.offset > -1) { - this.addParamInt(exp,'offset', this.offset); - exp.offset = this.offset; - } - if (this.count > -1) { - this.addParamInt(exp, 'count', this.count); - } - if (this.count > 0 && this.offset === -1) { - this.offset = 0; - } - - this.worker.opContext.log('start working'); - this.worker.deadCheck('expand'); - - let notClosed = { value : false}; - - try { - if (source.jsonObj.compose && Extensions.checkNoModifiers(source.jsonObj.compose, 'ValueSetExpander.Expand', 'compose')) { - await this.handleCompose(source, filter, exp, notClosed); - } - - if (this.requiredSupplements.length > 0) { - throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.worker.opContext.i18n.translatePlural(this.requiredSupplements.length, 'VALUESET_SUPPLEMENT_MISSING', this.params.httpLanguages, [this.requiredSupplements.join(', ')]), 'not-found', 400); - } - } catch (e) { - if (e instanceof Issue) { - if (e.finished) { - // nothing - we're just trapping this - if (this.totalStatus === 'uninitialised') { - this.totalStatus = 'off'; - } else if (e.toocostly) { - if (this.params.limitedExpansion) { - Extensions.addBoolean(exp, 'http://hl7.org/fhir/StructureDefinition/valueset-toocostly', 'value', true); - if (table != null) { - div_.p().style('color: Maroon').tx(e.message); - } - } else { - throw e; - } - } else { - // nothing- swallow it - } - } else { - throw e; - } - } else { - throw e; - } - } - - this.worker.opContext.log('finish up'); - - let list; - if (notClosed.value) { - Extensions.addBoolean(exp, 'http://hl7.org/fhir/StructureDefinition/valueset-unclosed', true); - list = this.fullList; - for (const c of this.fullList) { - c.contains = undefined; - } - if (table != null) { - div_.addTag('p').setAttribute('style', 'color: Navy').tx('Because of the way that this value set is defined, not all the possible codes can be listed in advance'); - } - } else { - if (this.totalStatus === 'off' || this.total === -1) { - this.canBeHierarchy = false; - } else if (this.total > 0) { - exp.total = this.total; - } else { - exp.total = this.fullList.length; - } - - if (this.canBeHierarchy && (this.count <= 0 || this.count > this.fullList.length)) { - list = this.rootList; - } else { - list = this.fullList; - for (const c of this.fullList) { - c.contains = undefined; - } - } - } - - if (this.offset + this.count < 0 && this.fullList.length > this.limit) { - this.log.log('Operation took too long @ expand (' + this.constructor.name + ')'); - throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [source.vurl, '>' + this.limit]), null, 400).withDiagnostics(this.worker.opContext.diagnostics()); - } else { - let t = 0; - let o = 0; - for (let i = 0; i < list.length; i++) { - this.worker.deadCheck('expand#1'); - const c = list[i]; - if (this.map.has(this.keyC(c))) { - o++; - if (o > this.offset && (this.count <= 0 || t < this.count)) { - t++; - if (!exp.contains) { - exp.contains = []; - } - exp.contains.push(c); - if (table != null) { - const tr = table.tr(); - tr.td().tx(c.system); - tr.td().tx(c.code); - tr.td().tx(c.display); - } - } - } - } - } - - for (const s of this.worker.foundParameters) { - const [l, r] = s.split('='); - if (r != source.vurl) { - this.addParamUri(exp, l, r); - } - } - - return result; - } - - checkResourceCanonicalStatus(exp, resource, source) { - if (resource.jsonObj) { - resource = resource.jsonObj; - } - this.checkCanonicalStatus(exp, this.worker.makeVurl(resource), resource.status, Extensions.readString(resource, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status'), resource.experimental, source); - } - - checkProviderCanonicalStatus(exp, cs, source) { - let status = cs.status(); - this.checkCanonicalStatus(exp, cs.vurl(), status.status, status.standardsStatus, status.experimental, source); - } - - checkCanonicalStatus(exp, vurl, status, standardsStatus, experimental, source) { - if (standardsStatus == 'deprecated') { - this.addParamUri(exp, 'warning-deprecated', vurl); - } else if (standardsStatus == 'withdrawn') { - this.addParamUri(exp, 'warning-withdrawn', vurl); - } else if (status == 'retired') { - this.addParamUri(exp, 'warning-retired', vurl); - } else if (experimental && !source.experimental) { - this.addParamUri(exp, 'warning-experimental', vurl) - } else if (((status == 'draft') || (standardsStatus == 'draft')) && - !((source.status == 'draft') || (Extensions.readString(source, 'http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status') == 'draft'))) { - this.addParamUri(exp, 'warning-draft', vurl) - } - } - - addParamStr(exp, name, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - exp.parameter.push({name: name, valueString: value}); - } - } - - addParamBool(exp, name, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - exp.parameter.push({name: name, valueBoolean: value}); - } - } - - addParamCode(exp, name, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - exp.parameter.push({name: name, valueCode: value}); - } - } - - addParamInt(exp, name, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - exp.parameter.push({name: name, valueInteger: value}); - } - } - - addParamUri(exp, name, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - exp.parameter.push({name: name, valueUri: value}); - } - } - - addParam(exp, name, valueName, value) { - if (!this.hasParam(exp, name, value)) { - if (!exp.parameter) { - exp.parameter = []; - } - let p = {name: name} - exp.parameter.push(p); - p[valueName] = value; - } - } - - - hasParam(exp, name, value) { - return (exp.parameter || []).find((ex => ex.name == name && getValuePrimitive(ex) == value)); - } - - isExcluded(system, version, code) { - return this.excluded.has(system+'|'+version+'#'+code); - } - - keyS(system, version, code) { - return system+"~"+(this.doingVersion ? version+"~" : "")+code; - } - - keyC(contains) { - return this.keyS(contains.system, contains.version, contains.code); - } - - defineProperty(expansion, contains, url, code, valueName, value) { - if (value === undefined || value == null) { - return; - } - if (!expansion.property) { - expansion.property = []; - } - let pd = expansion.property.find(t1 => t1.uri == url || t1.code == code); - if (!pd) { - pd = {}; - expansion.property.push(pd); - pd.uri = url; - pd.code = code; - } else if (!pd.uri) { - pd.uri = url - } - if (pd.uri != url) { - throw new Error('URL mismatch on expansion: ' + pd.uri + ' vs ' + url + ' for code ' + code); - } else { - code = pd.code; - } - - if (!contains.property) { - contains.property = []; - } - let pdv = contains.property.find(t2 => t2.code == code); - if (!pdv) { - pdv = {}; - contains.property.push(pdv); - pdv.code = code; - } - pdv[valueName] = value; - } - - addToTotal(t) { - if (this.total > -1 && this.totalStatus != "off") { - this.total = this.total + t; - this.totalStatus = 'set'; - } - } - - noTotal() { - this.total = -1; - this.totalStatus = 'off'; - } - - getPropUrl(cs, pn) { - for (let p of cs.propertyDefinitions()) { - if (pn == p.code) { - return p.uri; - } - } - return undefined; - } - - -} - -class ExpandWorker extends TerminologyWorker { +class RelatedWorker extends TerminologyWorker { /** * @param {OperationContext} opContext - Operation context * @param {Logger} log - Logger instance @@ -1533,19 +32,20 @@ class ExpandWorker extends TerminologyWorker { * @returns {string} */ opName() { - return 'expand'; + return 'related'; } /** - * Handle a type-level $expand request - * GET/POST /ValueSet/$expand + * Handle a type-level $related request + * GET/POST /ValueSet/$related * @param {express.Request} req - Express request * @param {express.Response} res - Express response */ async handle(req, res) { try { - await this.handleTypeLevelExpand(req, res); + await this.handleTypeLevelRelated(req, res); } catch (error) { + console.error(error); req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : ""); this.log.error(error); const statusCode = error.statusCode || 500; @@ -1571,14 +71,14 @@ class ExpandWorker extends TerminologyWorker { } /** - * Handle an instance-level $expand request - * GET/POST /ValueSet/{id}/$expand + * Handle an instance-level $related request + * GET/POST /ValueSet/{id}/$related * @param {express.Request} req - Express request * @param {express.Response} res - Express response */ async handleInstance(req, res) { try { - await this.handleInstanceLevelExpand(req, res); + await this.handleInstanceLevelRelated(req, res); } catch (error) { req.logInfo = this.usedSources.join("|")+" - error"+(error.msgId ? " "+error.msgId : ""); this.log.error(error); @@ -1599,247 +99,337 @@ class ExpandWorker extends TerminologyWorker { } /** - * Handle type-level expand: /ValueSet/$expand + * Handle type-level $related: /ValueSet/$related * ValueSet identified by url, or provided directly in body */ - async handleTypeLevelExpand(req, res) { - this.deadCheck('expand-type-level'); - - // Determine how the request is structured - let valueSet = null; - let params = null; - - if (req.method === 'POST' && req.body) { - if (req.body.resourceType === 'ValueSet') { - // Body is directly a ValueSet resource - valueSet = new ValueSet(req.body); - params = this.queryToParameters(req.query); - this.seeSourceVS(valueSet); - - } else if (req.body.resourceType === 'Parameters') { - // Body is a Parameters resource - params = req.body; + async handleTypeLevelRelated(req, res) { + this.deadCheck('related-type-level'); - // Check for valueSet parameter - const valueSetParam = this.findParameter(params, 'valueSet'); - if (valueSetParam && valueSetParam.resource) { - valueSet = new ValueSet(valueSetParam.resource); - this.seeSourceVS(valueSet); - } - - } else { - // Assume form body - convert to Parameters - params = this.formToParameters(req.body, req.query); - } - } else { - // GET request - convert query to Parameters - params = this.queryToParameters(req.query); - } + let params = req.body; this.addHttpParams(req, params); - - // Check for context parameter - not supported yet - const contextParam = this.findParameter(params, 'context'); - if (contextParam) { - return res.status(400).json(this.operationOutcome('error', 'not-supported', - 'The context parameter is not yet supported')); - } - - // Handle tx-resource and cache-id parameters this.setupAdditionalResources(params); - const logExtraOutput = this.findParameter(params, 'logExtraOutput'); - let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false); txp.readParams(params); - // If no valueSet yet, try to find by url - if (!valueSet) { - const urlParam = this.findParameter(params, 'url'); - const versionParam = this.findParameter(params, 'valueSetVersion'); + let thisVS = await this.readValueSet(res, "this", params, txp); + let otherVS = await this.readValueSet(res, "other", params, txp); - if (!urlParam) { - return res.status(400).json(this.operationOutcome('error', 'invalid', - 'Must provide either a ValueSet resource or a url parameter')); - } + const result = await this.doRelated(txp, thisVS, otherVS); + return res.json(this.fixForVersion(result)); + } + + /** + * Handle instance-level related: /ValueSet/{id}/$related + * ValueSet identified by resource ID + */ + async handleInstanceLevelRelated(req, res) { + this.deadCheck('related-instance-level'); - const url = this.getParameterValue(urlParam); - const version = versionParam ? this.getParameterValue(versionParam) : null; + let params = req.body; + this.addHttpParams(req, params); + this.setupAdditionalResources(params); + let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false); + txp.readParams(params); - valueSet = await this.findValueSet(url, version); - this.seeSourceVS(valueSet, url); - if (!valueSet) { - return res.status(404).json(this.operationOutcome('error', 'not-found', - version ? `ValueSet not found: ${url} version ${version}` : `ValueSet not found: ${url}`)); - } + const { id } = req.params; + // Find the ValueSet by ID + const thisVS = await this.provider.getValueSetById(this.opContext, id); + if (!thisVS) { + return res.status(404).json(this.operationOutcome('error', 'not-found', + `ValueSet/${id} not found`)); } + let otherVS = await this.readValueSet(res, "other", params, txp); - // Perform the expansion - const result = await this.doExpand(valueSet, txp, logExtraOutput); - req.logInfo = this.usedSources.join("|")+txp.logInfo(); + const result = await this.doRelated(valueSet, txp, logExtraOutput); return res.json(this.fixForVersion(result)); } /** - * Handle instance-level expand: /ValueSet/{id}/$expand - * ValueSet identified by resource ID + * Build an OperationOutcome + * @param {string} severity - error, warning, information + * @param {string} code - Issue code + * @param {string} message - Diagnostic message + * @returns {Object} OperationOutcome resource */ - async handleInstanceLevelExpand(req, res) { - this.deadCheck('expand-instance-level'); + operationOutcome(severity, code, message) { + return { + resourceType: 'OperationOutcome', + issue: [{ + severity, + code, + diagnostics: message + }] + }; + } - const { id } = req.params; + async readValueSet(res, prefix, params, txp) { + const valueSetParam = this.findParameter(params, prefix+'ValueSet'); + if (valueSetParam && valueSetParam.resource) { + let valueSet = new ValueSet(valueSetParam.resource); + this.seeSourceVS(valueSet); + return valueSet; + } + // If no valueSet yet, try to find by url + const urlParam = this.findParameter(params, prefix+'Url'); + const versionParam = this.findParameter(params, 'valueSetVersion'); - // Find the ValueSet by ID - const valueSet = await this.provider.getValueSetById(this.opContext, id); + if (!urlParam) { + return res.status(400).json(this.operationOutcome('error', 'invalid', + `Must provide either a ${prefix}ValueSet resource or a ${prefix}Url parameter`)); + } + + const url = this.getParameterValue(urlParam); + const version = versionParam ? this.getParameterValue(versionParam) : null; + let valueSet = await this.findValueSet(url, version); + this.seeSourceVS(valueSet, url); if (!valueSet) { return res.status(404).json(this.operationOutcome('error', 'not-found', - `ValueSet/${id} not found`)); - } - - // Parse parameters - let params; - if (req.method === 'POST' && req.body) { - if (req.body.resourceType === 'Parameters') { - params = req.body; - } else { - // Form body - params = this.formToParameters(req.body, req.query); - } + version ? `ValueSet not found: ${url} version ${version}` : `ValueSet not found: ${url}`)); } else { - params = this.queryToParameters(req.query); + return valueSet; } + } - // Check for context parameter - not supported yet - const contextParam = this.findParameter(params, 'context'); - if (contextParam) { - return res.status(400).json(this.operationOutcome('error', 'not-supported', - 'The context parameter is not yet supported')); + async doRelated(txp, thisVS, otherVS) { + // ok, we have to compare the composes. we don't care about anything else + const thisC = thisVS.jsonObj.compose; + const otherC = otherVS.jsonObj.compose; + if (!thisC) { + return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`); } + Extensions.checkNoModifiers(thisC, 'RelatedWorker.doRelated', 'compose') + this.checkNoLockedDate(thisVS.vurl, thisC); + if (!otherC) { + return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`); + } + Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose') + this.checkNoLockedDate(otherVS.vurl, otherC); - // Handle tx-resource and cache-id parameters - this.setupAdditionalResources(params); - const logExtraOutput = this.findParameter(params, 'logExtraOutput'); + // ok, first, if we can determine that the value sets match from the definitions, we will + // if that fails, then we have to do the expansions, and then decide - let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false); - txp.readParams(params); + // first, we sort the includes by system, and then compare them as a group + // Build a map of system -> { this: [...includes], other: [...includes] } + const systemMap = new Map(); + await this.addIncludes(systemMap, thisC.include || [], 'this', txp); + await this.addIncludes(systemMap, otherC.include || [], 'other', txp); + await this.addIncludes(systemMap, thisC.exclude || [], 'thisEx', txp); + await this.addIncludes(systemMap, otherC.exclude || [], 'otherEx', txp); - // Perform the expansion - const result = await this.doExpand(valueSet, txp, logExtraOutput); - req.logInfo = this.usedSources.join("|")+txp.logInfo(); - return res.json(this.fixForVersion(result)); + let status = { left: false, right: false, fail: false, common : false}; + + for (const [key, value] of systemMap.entries()) { + if (key) { + this.compareSystems(status, value); + } else { + this.compareNonSystems(status, value); + } + } + + // can't tell? OK, we need to do expansions. Note that + // expansions might not work (infinite value sets) so + // we can't tell. + if (status.fail) { + status.fail = false; + this.compareExpansions(status, thisC, otherC); + } + if (status.fail) { + return this.makeOutcome("indeterminate", `Unable to compare ${thisVS.vurl} and ${otherVS.vurl}: `+status.reason); + } else if (!status.common) { + return this.makeOutcome("disjoint", `No shared codes between the value sets ${thisVS.vurl} and ${otherVS.vurl}`); + } else if (!status.left && !status.right) { + return this.makeOutcome("same", `The value sets ${thisVS.vurl} and ${otherVS.vurl} contain the same codes`); + } else if (status.left && status.right) { + return this.makeOutcome("overlapping", `Both value sets ${thisVS.vurl} and ${otherVS.vurl} contain the codes the other doesn't, but there is some overlap`); + } else if (status.left) { + return this.makeOutcome("superset", `The valueSet ${thisVS.vurl} is a super-set of the valueSet ${otherVS.vurl}`); + } else { + return this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a seb-set of the valueSet ${otherVS.vurl}`); + } } - // Note: setupAdditionalResources, queryToParameters, formToParameters, - // findParameter, getParameterValue, and wrapRawResource are inherited - // from TerminologyWorker base class + async addIncludes(systemMap, includes, side, txp) { + for (const inc of includes) { + let key = inc.system || ''; + if (await this.versionMatters(key, inc.version, txp)) { + key = key + "|" + version; + } + if (!systemMap.has(key)) { + systemMap.set(key, {this: [], other: []}); + } + systemMap.get(key)[side].push(inc); + } + } - /** - * Perform the actual expansion operation - * Uses expansion cache for expensive operations - * @param {Object} valueSet - ValueSet resource to expand - * @param {Object} params - Parameters resource with expansion options - * @returns {Object} Expanded ValueSet resource - */ - async doExpand(valueSet, params, logExtraOutput) { - this.deadCheck('doExpand'); + async versionMatters(key, version, txp) { + let cs = await this.findCodeSystem(key, version, txp, ['complete', 'fragment'], null, true); + return cs == null || cs.versionNeeded(); + } - const expansionCache = this.opContext.expansionCache; - // Compute cache key (only if caching is available and not debugging) - let cacheKey = null; - if (expansionCache && (CACHE_WHEN_DEBUGGING || !this.opContext.debugging)) { - cacheKey = expansionCache.computeKey(valueSet, params, this.additionalResources); + compareNonSystems(status, value) { + // not done yet + status.fail = true; + } - // Check for cached expansion - const cached = expansionCache.get(cacheKey); - if (cached) { - this.log.debug('Using cached expansion'); - return cached; + compareSystems(status, value) { + if (value.thisEx || value.otherEx) { + // we don't try in this case + status.fail = true; + status.common = true; + } else if (!value.this) { + // left has nothing for this one. + status.right = true; + status.common = true; + } else if (!value.other) { + status.left = true; + status.common = true; + } else { + // for now, we don't do value set imports + if (this.hasValueSets(value.this) || this.hasValueSets(value.other)) { + status.fail = true; + return; + } + if (this.hasConceptsAndFilters(value.this) || this.hasConceptsAndFilters(value.other)) { + status.fail = true; + return; + } + // we have includes on both sides. We might have full system, a list, or a filter. we don't care about order. so clean up and sort + this.tidyIncludes(value.this); + this.tidyIncludes(value.other); + // if both sides have full include, they match, period. + if (this.isFullSystem(value.this[0]) && this.isFullSystem(value.other[0])) { + status.common = true; + return; + } else if (this.isFullSystem(value.this[0])) { + status.common = true; + status.left = true; + return; + } else if (this.isFullSystem(value.this[0])) { + status.common = true; + status.right = true; + return; + } else if (isConcepts(value.this[0]) && isConcepts(value.other[0])) { + this.compareCodeLists(status, value.this[0], value.other[0]); + return; + } else if (isFilter(value.this[0]) && isFilter(value.other[0])) { + if (value.this.length != value.other.length) { + status.fail = true; + return; + } else { + for (let i = 0; i < value.this.length; i++) { + let t = value.this.get(i); + let o = value.other.get(i); + if (!filtersMatch(t, o)) { + status.fail = true; + return; + } + status.common = true; + return; + } + } } } + status.fail = true; // not sure why we got to here, but it doesn't matter: we can't tell + } - // Perform the actual expansion - const startTime = performance.now(); - const result = await this.performExpansion(valueSet, params, logExtraOutput); - const durationMs = performance.now() - startTime; - - // Cache if it took long enough (and not debugging) - if (cacheKey && expansionCache && (CACHE_WHEN_DEBUGGING || !this.opContext.debugging)) { - const wasCached = expansionCache.set(cacheKey, result, durationMs); - if (wasCached) { - this.log.debug(`Cached expansion (took ${Math.round(durationMs)}ms)`); + hasValueSets(list) { + for (const inc of list) { + if (inc.valueSet) { + return true; } } - - return result; + return false; } - /** - * Perform the actual expansion logic - * @param {Object} valueSet - ValueSet resource to expand - * @param {Object} params - Parameters resource with expansion options - * @returns {Object} Expanded ValueSet resource - */ - async performExpansion(valueSet, params, logExtraOutput) { - this.deadCheck('performExpansion'); + hasConceptsAndFilters(list) { + for (const inc of list) { + if (inc.concept?.length > 0 && inc.filter?.length > 0) { + return true; + } + } + return false; + } - // Store params for worker methods - this.params = params; + tidyIncludes(list) { + let collector = null; + for (let i = list.length - 1; i >= 0; i--) { + const inc = list[i]; + if (inc.system && inc.concept && !inc.filter) { + if (collector) { + collector.concept.push(...inc.concept); + list.splice(i, 1); + } else { + collector = inc; + } + } + } + for (let inc of list) { + if (inc.concept) { + inc.concept.sort((a, b) => (a.code || '').localeCompare(b.code)); + } + if (inc.filter) { + inc.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value)); + } + } + function includeRank(inc) { + if (!inc.system) return 0; + const hasConcepts = inc.concept?.length > 0; + const hasFilters = inc.filter?.length > 0; + if (!hasConcepts && !hasFilters) return 1; + if (hasConcepts && !hasFilters) return 2; + if (!hasConcepts && hasFilters) return 3; + return 4; + } - if (params.limit < -1) { - params.limit = -1; - } else if (params.limit > UPPER_LIMIT_TEXT) { - params.limit = UPPER_LIMIT_TEXT; // can't ask for more than this externally, though you can internally + function compareFilter(a, b) { + const af = a.filter?.[0]; + const bf = b.filter?.[0]; + if (!af && !bf) return 0; + if (!af) return -1; + if (!bf) return 1; + return (af.property || '').localeCompare(bf.property || '') || + (af.op || '').localeCompare(bf.op || '') || + (af.value || '').localeCompare(bf.value || ''); } - const filter = new SearchFilterText(params.filter); - //txResources = processAdditionalResources(context, manager, nil, params); - // Create expander and run expansion - const expander = new ValueSetExpander(this, params); - expander.logExtraOutput = logExtraOutput; - return await expander.expand(valueSet, filter); + list.sort((a, b) => + includeRank(a) - includeRank(b) || + compareFilter(a, b) + ); } - /** - * Generate a UUID - * @returns {string} UUID - */ - generateUuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + compareCodeLists(status, first, first2) { + const tSet = new Set(t.map(x => x.code)); + const oSet = new Set(o.map(x => x.code)); + + status.common = [...tSet].filter(c => oSet.has(c)).length > 0; + status.left = [...tSet].filter(c => !oSet.has(c)).length > 0; + status.right = [...oSet].filter(c => !tSet.has(c)).length > 0; } - /** - * Build an OperationOutcome - * @param {string} severity - error, warning, information - * @param {string} code - Issue code - * @param {string} message - Diagnostic message - * @returns {Object} OperationOutcome resource - */ - operationOutcome(severity, code, message) { - return { - resourceType: 'OperationOutcome', - issue: [{ - severity, - code, - diagnostics: message - }] + makeOutcome(code, msg) { + const parameters = { + resourceType: 'Parameters', + parameter: [ + {name: 'result', valueCode: code} + ] }; + if (msg) { + parameters.parameter.push({name: 'message', valueString: msg}) + } + return parameters; + } + + isFullSystem(inc) { + return !inc.concept && !inc.filter; } + compareExpansions(status, thisC, otherC) { + + } } module.exports = { - ExpandWorker, - ValueSetExpander, - ValueSetCounter, - ImportedValueSet, - ValueSetFilterContext, - EmptyFilterContext, - TotalStatus, - UPPER_LIMIT_NO_TEXT, - UPPER_LIMIT_TEXT, - INTERNAL_LIMIT, - EXPANSION_DEAD_TIME_SECS + RelatedWorker }; \ No newline at end of file diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 959cf84..2c71585 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -520,7 +520,7 @@ class ValueSetChecker { result = false; cause.value = 'code-invalid'; this.worker.opContext.addNote(this.valueSet, 'Unknown code', this.indentCount); - let msg = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]); + let msg = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system, cs.version()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]); messages.push(msg); op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', msg, 'invalid-code')); } @@ -612,7 +612,7 @@ class ValueSetChecker { result = false; cause.value = 'code-invalid'; this.worker.opContext.addNote(this.valueSet, 'Unknown code', this.indentCount); - let msg = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(system), this.params.HTTPLanguages, [code, system, version, SCTVersion(system, version)]); + let msg = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(system, version), this.params.HTTPLanguages, [code, system, version, SCTVersion(system, version)]); messages.push(msg); op.addIssue(new Issue('warning', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', msg, 'invalid-code')); } @@ -748,7 +748,7 @@ class ValueSetChecker { for (let cc of this.valueSet.jsonObj.compose.exclude || []) { this.worker.deadCheck('check#4'); let excluded; - if (cc.system) { + if (!cc.system) { excluded = true; } else { let cs = await this.worker.findCodeSystem(cc.system, cc.version, this.params, ['complete', 'fragment'], op,true, true, false); @@ -1185,7 +1185,7 @@ class ValueSetChecker { ts.push(vs); let m; if (prov.contentMode() === 'complete') { - m = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(ws), this.params.HTTPLanguages, [c.code, ws, prov.version(), SCTVersion(ws, prov.version())]); + m = this.worker.i18n.translate(Unknown_Code_in_VersionSCT(ws, prov.version()), this.params.HTTPLanguages, [c.code, ws, prov.version(), SCTVersion(ws, prov.version())]); cause.value = 'code-invalid'; msg(m); op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', m, 'invalid-code'), true); @@ -1499,8 +1499,8 @@ class ValueSetChecker { op.addIssue(new Issue('warning', 'code-invalid', addToPath(path, 'code'), 'UNKNOWN_CODE_IN_FRAGMENT', this.worker.i18n.translate('UNKNOWN_CODE_IN_FRAGMENT', this.params.HTTPLanguages, [code, cs.system(), cs.version()]), 'invalid-code')); result = true; } else { - op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', - this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]), 'invalid-code')); + op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), cs.version() ? 'Unknown_Code_in_Version' : 'Unknown_Code_in', + this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system(), cs.version()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]), 'invalid-code')); } } if (loc.message && op) { @@ -1768,7 +1768,7 @@ class ValueSetChecker { if (loc === null || loc.context == null) { if (!this.params.membershipOnly) { op.addIssue(new Issue('error', 'code-invalid', addToPath(path, 'code'), 'Unknown_Code_in_Version', - this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]), 'invalid-code')); + this.worker.i18n.translate(Unknown_Code_in_VersionSCT(cs.system(), cs.version()), this.params.HTTPLanguages, [code, cs.system(), cs.version(), SCTVersion(cs.system(), cs.version())]), 'invalid-code')); } } else if (!(abstractOk || !cs.IsAbstract(loc.context))) { if (!this.params.membershipOnly) { @@ -1801,11 +1801,13 @@ function addToPath(path, name) { } -function Unknown_Code_in_VersionSCT(url) { +function Unknown_Code_in_VersionSCT(url, version) { if (url === 'http://snomed.info/sct') { return 'Unknown_Code_in_Version_SCT'; - } else { + } else if (version) { return 'Unknown_Code_in_Version'; + } else { + return 'Unknown_Code_in'; } } @@ -1940,7 +1942,12 @@ class ValidateWorker extends TerminologyWorker { // Get the CodeSystem - from parameter or by url const codeSystem = await this.resolveCodeSystem(params, txp, coded?.coding?.[0] ?? null, mode); if (!codeSystem) { - throw new Issue('error', 'invalid', null, null, 'No CodeSystem specified - provide url parameter or codeSystem resource', null, 400); + if (!coded?.coding?.[0].system) { + let msg = this.i18n.translate('Coding_has_no_system__cannot_validate', txp.HTTPLanguages, []); + throw new Issue('warning', 'invalid', mode.issuePath, 'Coding_has_no_system__cannot_validate', msg, 'invalid-data'); + } else { + throw new Issue('error', 'invalid', null, null, 'No CodeSystem specified - provide url parameter or codeSystem resource', null, 400); + } } if (codeSystem.contentMode() == 'supplement') { throw new Issue('error', 'invalid', this.systemPath(mode), 'CODESYSTEM_CS_NO_SUPPLEMENT', this.opContext.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', txp.HTTPLanguages, [codeSystem.vurl()]), "invalid-data"); @@ -2482,9 +2489,6 @@ class ValidateWorker extends TerminologyWorker { if (coded.coding[0].code) { p.addParamCode('code', coded.coding[0].code) } - if (coded.coding[0].display) { - p.addParamStr('display', coded.coding[0].display) - } } if (error.unknownSystem) { p.addParamCanonical("x-caused-by-unknown-system", error.unknownSystem); diff --git a/tx/workers/worker.js b/tx/workers/worker.js index 233de2a..95402c9 100644 --- a/tx/workers/worker.js +++ b/tx/workers/worker.js @@ -169,13 +169,13 @@ class TerminologyWorker { if (!provider && !nullOk) { if (!version) { - throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_EXP", this.i18n.translate("UNKNOWN_CODESYSTEM_EXP", params.FHTTPLanguages, [url]), "not-found", 404); + throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_EXP", this.i18n.translate("UNKNOWN_CODESYSTEM_EXP", params.FHTTPLanguages, [url]), "not-found", 422); } else { const versions = await this.listVersions(url); if (versions.length === 0) { - throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_VERSION_EXP_NONE", this.i18n.translate("UNKNOWN_CODESYSTEM_VERSION_EXP_NONE", params.FHTTPLanguages, [url, version]), "not-found", 404); + throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_VERSION_EXP_NONE", this.i18n.translate("UNKNOWN_CODESYSTEM_VERSION_EXP_NONE", params.FHTTPLanguages, [url, version]), "not-found", 422); } else { - throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_VERSION_EXP", this.i18n.translate("UNKNOWN_CODESYSTEM_VERSION_EXP", params.FHTTPLanguages, [url, version, this.presentVersionList(versions)]), "not-found", 404); + throw new Issue("error", "not-found", null, "UNKNOWN_CODESYSTEM_VERSION_EXP", this.i18n.translate("UNKNOWN_CODESYSTEM_VERSION_EXP", params.FHTTPLanguages, [url, version, this.presentVersionList(versions)]), "not-found", 422); } } } From 24654b10f412d59bd89fbc6b055c6f57001285f9 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 8 Feb 2026 23:37:44 +1100 Subject: [PATCH 2/3] lint driven changes --- library/languages.js | 32 +----------------------------- tests/library/languages.test.js | 2 -- tx/cs/cs-ucum.js | 1 - tx/workers/related.js | 35 ++++++++++++++++++++++----------- 4 files changed, 25 insertions(+), 45 deletions(-) diff --git a/library/languages.js b/library/languages.js index 7434f7f..4cae4e0 100644 --- a/library/languages.js +++ b/library/languages.js @@ -1,6 +1,6 @@ const fs = require('fs'); const os = require('os'); -const {validateParameter, validateOptionalParameter, Utilities} = require("./utilities"); +const {validateOptionalParameter, Utilities} = require("./utilities"); /** * Language part types for matching depth @@ -667,36 +667,6 @@ class LanguageDefinitions { this.variants.set(variant.code, variant); } - /** - * Parse and validate a language code - * - * @return {Language} parsed language (or null) - */ - parse(code) { - if (!code) return null; - - // Check cache first - if (this.parsed.has(code)) { - return this.parsed.get(code); - } - - const parts = code.split('-'); - let index = 0; - - // Validate language - if (index >= parts.length) return null; - const langCode = parts[index].toLowerCase(); - if (!this.languages.has(langCode) && langCode !== '*') { - return null; // Invalid language code - } - - const lang = new Language(code); - - // Cache the result - this.parsed.set(code, lang); - return lang; - } - /** * Parse and validate a language code * diff --git a/tests/library/languages.test.js b/tests/library/languages.test.js index eaeb1d0..2eb6e71 100644 --- a/tests/library/languages.test.js +++ b/tests/library/languages.test.js @@ -10,8 +10,6 @@ const path = require('path'); const {TestUtilities} = require("../test-utilities"); describe('Language Class', () => { - let languageDefinitions; - beforeEach(async () => { this.languageDefinitions = await TestUtilities.loadLanguageDefinitions(); }); diff --git a/tx/cs/cs-ucum.js b/tx/cs/cs-ucum.js index a7829d9..3c188c1 100644 --- a/tx/cs/cs-ucum.js +++ b/tx/cs/cs-ucum.js @@ -4,7 +4,6 @@ */ const { CodeSystemProvider, FilterExecutionContext, CodeSystemFactoryProvider} = require('./cs-api'); -const { CodeSystem } = require("../library/codesystem"); const ValueSet = require("../library/valueset"); const assert = require('assert'); const {UcumService} = require("../library/ucum-service"); diff --git a/tx/workers/related.js b/tx/workers/related.js index 6a40c03..4843e3b 100644 --- a/tx/workers/related.js +++ b/tx/workers/related.js @@ -140,7 +140,7 @@ class RelatedWorker extends TerminologyWorker { } let otherVS = await this.readValueSet(res, "other", params, txp); - const result = await this.doRelated(valueSet, txp, logExtraOutput); + const result = await this.doRelated(txp, thisVS, otherVS); return res.json(this.fixForVersion(result)); } @@ -162,7 +162,7 @@ class RelatedWorker extends TerminologyWorker { }; } - async readValueSet(res, prefix, params, txp) { + async readValueSet(res, prefix, params) { const valueSetParam = this.findParameter(params, prefix+'ValueSet'); if (valueSetParam && valueSetParam.resource) { let valueSet = new ValueSet(valueSetParam.resource); @@ -252,8 +252,9 @@ class RelatedWorker extends TerminologyWorker { async addIncludes(systemMap, includes, side, txp) { for (const inc of includes) { let key = inc.system || ''; - if (await this.versionMatters(key, inc.version, txp)) { - key = key + "|" + version; + let v = {}; + if (await this.versionMatters(key, inc.version, v, txp)) { + key = key + "|" + v.version; } if (!systemMap.has(key)) { systemMap.set(key, {this: [], other: []}); @@ -262,12 +263,16 @@ class RelatedWorker extends TerminologyWorker { } } - async versionMatters(key, version, txp) { + async versionMatters(key, version, v, txp) { let cs = await this.findCodeSystem(key, version, txp, ['complete', 'fragment'], null, true); - return cs == null || cs.versionNeeded(); + let res = cs == null || cs.versionNeeded(); + if (res) { + v.version = version || cs.version(); + } + return res; } - compareNonSystems(status, value) { + compareNonSystems(status) { // not done yet status.fail = true; } @@ -305,14 +310,14 @@ class RelatedWorker extends TerminologyWorker { status.common = true; status.left = true; return; - } else if (this.isFullSystem(value.this[0])) { + } else if (this.isFullSystem(value.other[0])) { status.common = true; status.right = true; return; - } else if (isConcepts(value.this[0]) && isConcepts(value.other[0])) { + } else if (this.isConcepts(value.this[0]) && this.isConcepts(value.other[0])) { this.compareCodeLists(status, value.this[0], value.other[0]); return; - } else if (isFilter(value.this[0]) && isFilter(value.other[0])) { + } else if (this.isFilter(value.this[0]) && this.isFilter(value.other[0])) { if (value.this.length != value.other.length) { status.fail = true; return; @@ -426,7 +431,15 @@ class RelatedWorker extends TerminologyWorker { } compareExpansions(status, thisC, otherC) { - + return false; + } + + isConcepts(t) { + return false; + } + + isFilter(t) { + return false; } } From d1d248b094f3c84da8136c0f3f0cef22987a937e Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sun, 8 Feb 2026 23:40:54 +1100 Subject: [PATCH 3/3] more lint --- tx/workers/related.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tx/workers/related.js b/tx/workers/related.js index 4843e3b..ef53eaa 100644 --- a/tx/workers/related.js +++ b/tx/workers/related.js @@ -325,7 +325,7 @@ class RelatedWorker extends TerminologyWorker { for (let i = 0; i < value.this.length; i++) { let t = value.this.get(i); let o = value.other.get(i); - if (!filtersMatch(t, o)) { + if (!this.filtersMatch(t, o)) { status.fail = true; return; } @@ -404,7 +404,7 @@ class RelatedWorker extends TerminologyWorker { ); } - compareCodeLists(status, first, first2) { + compareCodeLists(status, t, o) { const tSet = new Set(t.map(x => x.code)); const oSet = new Set(o.map(x => x.code)); @@ -430,15 +430,19 @@ class RelatedWorker extends TerminologyWorker { return !inc.concept && !inc.filter; } - compareExpansions(status, thisC, otherC) { + compareExpansions() { // status, thisC, otherC) { return false; } - isConcepts(t) { + isConcepts() { return false; } - isFilter(t) { + isFilter() { + return false; + } + + filtersMatch() { return false; } }