From 72f1cc49661e64884ce6e0c5fc9fbc37acfa0eed Mon Sep 17 00:00:00 2001 From: Patrick Skowronek Date: Wed, 18 Dec 2024 14:40:33 +0100 Subject: [PATCH 1/4] chore: changed stratifier to dktk sample centric search --- packages/demo/public/options-ccp-demo.json | 19 +- packages/demo/src/AppCCP.svelte | 4 +- .../src/backends/ast-to-cql-translator.ts | 5 - .../demo/src/backends/cqlquery-mappings.ts | 63 ------ packages/demo/src/measures.ts | 44 ++++ .../ast-to-cql-translator.ts | 210 +++++++++++++++++- .../cqlquery-mappings.ts | 28 +-- packages/lib/src/types/ast.ts | 55 +++++ 8 files changed, 316 insertions(+), 112 deletions(-) diff --git a/packages/demo/public/options-ccp-demo.json b/packages/demo/public/options-ccp-demo.json index b6818904..dbc4f033 100644 --- a/packages/demo/public/options-ccp-demo.json +++ b/packages/demo/public/options-ccp-demo.json @@ -283,13 +283,12 @@ } ] }, - "negotiateOptions": { - "negotiateApp": "project-manager", - "newProjectUrl": "https://e260-ccp-req/project-manager/create-query-and-design-project", - "editProjectUrl": "https://e260-ccp-req/project-manager/edit-project", - "siteMapping": [ + "projectmanagerOptions": { + "newProjectUrl": "https://requests.dktk.dkfz.de/project-manager-api/create-query-and-design-project", + "editProjectUrl": "https://requests.dktk.dkfz.de/project-manager-api/edit-project", + "siteMappings": [ { - "site": "dktk-test", + "site": "DKTK-Test", "collection": "lens-dktk-test" }, { @@ -300,6 +299,10 @@ "site": "Berlin", "collection": "lens-berlin" }, + { + "site": "Berlin (OBDS)", + "collection": "lens-berlin" + }, { "site": "Dresden", "collection": "lens-dresden" @@ -325,11 +328,11 @@ "collection": "lens-mainz" }, { - "site": "München LMU", + "site": "München(LMU)", "collection": "lens-muenchen-lmu" }, { - "site": "München TUM", + "site": "München(TUM)", "collection": "lens-muenchen-tum" }, { diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index d5b9985b..ea29cc49 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -9,7 +9,7 @@ dktkMedicationStatementsMeasure, dktkPatientsMeasure, dktkProceduresMeasure, - dktkSpecimenMeasure, + dktkSpecificSpecimenMeasure, dktkHistologyMeasure, } from "./measures"; @@ -65,7 +65,7 @@ measures: [ dktkPatientsMeasure as MeasureItem, dktkDiagnosisMeasure as MeasureItem, - dktkSpecimenMeasure as MeasureItem, + dktkSpecificSpecimenMeasure as MeasureItem, dktkProceduresMeasure as MeasureItem, dktkMedicationStatementsMeasure as MeasureItem, dktkHistologyMeasure as MeasureItem, diff --git a/packages/demo/src/backends/ast-to-cql-translator.ts b/packages/demo/src/backends/ast-to-cql-translator.ts index 6b6a8148..f73d2592 100644 --- a/packages/demo/src/backends/ast-to-cql-translator.ts +++ b/packages/demo/src/backends/ast-to-cql-translator.ts @@ -110,11 +110,8 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { if (myCQL) { switch (myCriterion.type) { case "gender": - case "BBMRI_gender": case "histology": case "conditionValue": - case "BBMRI_conditionValue": - case "BBMRI_conditionSampleDiagnosis": case "conditionBodySite": case "conditionLocalization": case "observation": @@ -125,8 +122,6 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { case "procedureResidualstatus": case "medicationStatement": case "specimen": - case "BBMRI_specimen": - case "BBMRI_hasSpecimen": case "hasSpecimen": case "Organization": case "observationMolecularMarkerName": diff --git a/packages/demo/src/backends/cqlquery-mappings.ts b/packages/demo/src/backends/cqlquery-mappings.ts index 12bcac63..14625a6d 100644 --- a/packages/demo/src/backends/cqlquery-mappings.ts +++ b/packages/demo/src/backends/cqlquery-mappings.ts @@ -207,69 +207,6 @@ export const cqltemplate = new Map([ "(exists ([Observation: Code '21908-9' from loinc] O where O.value.coding.code contains '{{C}}')) or (exists ([Observation: Code '21902-2' from loinc] O where O.value.coding.code contains '{{C}}'))", ], ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"], - - ["BBMRI_gender", "Patient.gender"], - [ - "BBMRI_conditionSampleDiagnosis", - "((exists[Condition: Code '{{C}}' from {{A1}}]) or (exists[Condition: Code '{{C}}' from {{A2}}])) or (exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/SampleDiagnosis').value.coding.code contains '{{C}}'))", - ], - ["BBMRI_conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"], - [ - "BBMRI_conditionRangeDate", - "exists from [Condition] C\nwhere FHIRHelpers.ToDateTime(C.onset) between {{D1}} and {{D2}}", - ], - [ - "BBMRI_conditionRangeAge", - "exists from [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between Ceiling({{D1}}) and Ceiling({{D2}})", - ], - ["BBMRI_age", "AgeInYears() between Ceiling({{D1}}) and Ceiling({{D2}})"], - [ - "BBMRI_observation", - "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", - ], - [ - "BBMRI_observationSmoker", - "exists from [Observation: Code '72166-2' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", - ], - [ - "BBMRI_observationRange", - "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value between {{D1}} and {{D2}}", - ], - [ - "BBMRI_observationBodyWeight", - "exists from [Observation: Code '29463-7' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg' and (O.value as Quantity) > {{D2}} 'kg')", - ], - [ - "BBMRI_observationBMI", - "exists from [Observation: Code '39156-5' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg/m2' and (O.value as Quantity) > {{D2}} 'kg/m2')", - ], - ["BBMRI_hasSpecimen", "exists [Specimen]"], - ["BBMRI_specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], - ["BBMRI_retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"], - [ - "BBMRI_retrieveSpecimenByTemperature", - "(S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding.code contains '{{C}}')", - ], - [ - "BBMRI_retrieveSpecimenBySamplingDate", - "(FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}})", - ], - [ - "BBMRI_retrieveSpecimenByFastingStatus", - "(S.collection.fastingStatus.coding.code contains '{{C}}')", - ], - [ - "BBMRI_samplingDate", - "exists from [Specimen] S\nwhere FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}}", - ], - [ - "BBMRI_fastingStatus", - "exists from [Specimen] S\nwhere S.collection.fastingStatus.coding.code contains '{{C}}'", - ], - [ - "BBMRI_storageTemperature", - "exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding contains Code '{{C}}' from {{A1}})", - ], ]); export const criterionMap = new Map( diff --git a/packages/demo/src/measures.ts b/packages/demo/src/measures.ts index ab5acc7d..16473add 100644 --- a/packages/demo/src/measures.ts +++ b/packages/demo/src/measures.ts @@ -913,6 +913,50 @@ DKTK_STRAT_SPECIMEN_STRATIFIER `, }; +export const dktkSpecificSpecimenMeasure = { + key: "specimen", + measure: { + code: { + text: "specimen", + }, + extension: [ + { + url: "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + valueCode: "Specimen", + }, + ], + population: [ + { + code: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/measure-population", + code: "initial-population", + }, + ], + }, + criteria: { + language: "text/cql-identifier", + expression: "Specimen", + }, + }, + ], + stratifier: [ + { + code: { + text: "sample_kind", + }, + criteria: { + language: "text/cql", + expression: "SampleType", + }, + }, + ], + }, + cql: ` +DKTK_REPLACE_SPECIMEN_STRATIFIER`, +}; + export const dktkProceduresMeasure = { key: "procedures", measure: { diff --git a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts index f31cd033..1ffd7b90 100644 --- a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts +++ b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts @@ -1,11 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * TODO: Document this file. Move to Project */ -import type { - AstBottomLayerValue, - AstElement, - AstTopLayer, +import { + isAstTopLayer, + type AstBottomLayerValue, + type AstElement, + type AstTopLayer, } from "../types/ast"; import { alias as aliasMap, @@ -45,6 +47,33 @@ export const translateAstToCql = ( singletons = backendMeasures; singletons += resolveOperation(query); + let retrievalCriteria: string = "if InInitialPopulation then "; + + const additionalCriteria = processAdditionalCriterion(query); + if ( + additionalCriteria == "" || + additionalCriteria.substring(additionalCriteria.length - 1) == "(" + ) { + retrievalCriteria += "[Specimen]"; + } else if ( + additionalCriteria.substring(additionalCriteria.length - 9) == + "intersect" + ) { + retrievalCriteria += "[Specimen] S where " + additionalCriteria; + retrievalCriteria = retrievalCriteria.slice(0, -10); + } else { + retrievalCriteria += "[Specimen] S where " + additionalCriteria; + retrievalCriteria = retrievalCriteria.slice(0, -5); + } + + retrievalCriteria = retrievalCriteria += " else {} as List"; + const specimenMeasure = measures.find( + (element) => element.key == "specimen", + ); + if (specimenMeasure?.cql) { + specimenMeasure.cql = specimenMeasure?.cql + retrievalCriteria; + } + if (query.children.length == 0) { singletons += "\ntrue"; } @@ -53,6 +82,16 @@ export const translateAstToCql = ( return singletons; } + console.log( + cqlHeader + + getCodesystems() + + "context Patient\n" + + measures + .map((measureItem: MeasureItem) => measureItem.cql) + .join("") + + singletons, + ); + return ( cqlHeader + getCodesystems() + @@ -62,6 +101,166 @@ export const translateAstToCql = ( ); }; +const processAdditionalCriterion = (query: any): string => { + let additionalCriteria = ""; + + if (isAstTopLayer(query)) { + const top: AstTopLayer = query; + top.children.forEach(function (child) { + additionalCriteria += processAdditionalCriterion(child); + }); + } else { + const buttom: AstBottomLayerValue = query; + additionalCriteria += getRetrievalCriterion(buttom); + } + + return additionalCriteria; +}; + +const getRetrievalCriterion = (criterion: AstBottomLayerValue): string => { + let expression: string = ""; + let myCQL: string = ""; + const myCriterion = criterionMap.get(criterion.key); + if (myCriterion) { + switch (myCriterion.type) { + case "specimen": { + expression += "("; + myCQL += cqltemplate.get("retrieveSpecimenByType"); + if (typeof criterion.value === "string") { + if (criterion.value.slice(-1) === "%") { + const mykey = criterion.value.slice(0, -2); + if (criteria.values != undefined) { + criterion.value = criteria.values + .filter( + (value) => value.key.indexOf(mykey) != -1, + ) + .map((value) => value.key); + getRetrievalCriterion(criterion); + } + } else { + expression += + substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value as string, + ) + ") and\n"; + } + } + if (criterion.value instanceof Array) { + const values: string[] = []; + criterion.value.forEach((element) => { + values.push(element); + }); + + if (criterion.value.includes("blood-plasma")) { + values.push( + "plasma-edta", + "plasma-citrat", + "plasma-heparin", + "plasma-cell-free", + "plasma-other", + "plasma", + ); + } + if (criterion.value.includes("blood-serum")) { + values.push("serum"); + } + if (criterion.value.includes("tissue-ffpe")) { + values.push( + "tumor-tissue-ffpe", + "normal-tissue-ffpe", + "other-tissue-ffpe", + "tissue-formalin", + ); + } + if (criterion.value.includes("tissue-frozen")) { + values.push( + "tumor-tissue-frozen", + "normal-tissue-frozen", + "other-tissue-frozen", + ); + } + if (criterion.value.includes("dna")) { + values.push("cf-dna", "g-dna"); + } + if (criterion.value.includes("tissue-other")) { + values.push("tissue-paxgene-or-else", "tissue"); + } + if (criterion.value.includes("derivative-other")) { + values.push("derivative"); + } + if (criterion.value.includes("liquid-other")) { + values.push("liquid"); + } + + if (values.length === 1) { + expression += + substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + values[0], + ) + ") and\n"; + } else { + values.forEach((value: string) => { + expression += + "(" + + substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + value, + ) + + ") or\n"; + }); + expression = expression.slice(0, -4) + ") and\n"; + } + } + break; + } + case "samplingDate": { + expression += "("; + myCQL += cqltemplate.get("retrieveSpecimenBySamplingDate"); + + let newCQL: string = ""; + if ( + typeof criterion.value == "object" && + !(criterion.value instanceof Array) && + (criterion.value.min instanceof Date || + criterion.value.max instanceof Date) + ) { + if (!(criterion.value.min instanceof Date)) { + newCQL = myCQL.replace( + "between {{D1}} and {{D2}}", + "<= {{D2}}", + ); + } else if (!(criterion.value.max instanceof Date)) { + newCQL = myCQL.replace( + "between {{D1}} and {{D2}}", + ">= {{D1}}", + ); + } else { + newCQL = myCQL; + } + expression += + substituteCQLExpressionDate( + criterion.key, + myCriterion.alias, + newCQL, + "", + criterion.value.min as Date, + criterion.value.max as Date, + ) + ") and\n"; + } + break; + } + } + } + + return expression; +}; + const resolveOperation = (operation: AstElement): string => { let expression: string = ""; @@ -112,11 +311,8 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { switch (myCriterion.type) { case "gender": case "pseudo_projects": - case "BBMRI_gender": case "histology": case "conditionValue": - case "BBMRI_conditionValue": - case "BBMRI_conditionSampleDiagnosis": case "conditionBodySite": case "conditionLocalization": case "observation": diff --git a/packages/lib/src/cql-translator-service/cqlquery-mappings.ts b/packages/lib/src/cql-translator-service/cqlquery-mappings.ts index a37bb498..76be5b62 100644 --- a/packages/lib/src/cql-translator-service/cqlquery-mappings.ts +++ b/packages/lib/src/cql-translator-service/cqlquery-mappings.ts @@ -247,33 +247,7 @@ export const cqltemplate = new Map([ "BBMRI_observationBMI", "exists from [Observation: Code '39156-5' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg/m2' and (O.value as Quantity) > {{D2}} 'kg/m2')", ], - ["BBMRI_hasSpecimen", "exists [Specimen]"], - ["BBMRI_specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], - ["BBMRI_retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"], - [ - "BBMRI_retrieveSpecimenByTemperature", - "(S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding.code contains '{{C}}')", - ], - [ - "BBMRI_retrieveSpecimenBySamplingDate", - "(FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}})", - ], - [ - "BBMRI_retrieveSpecimenByFastingStatus", - "(S.collection.fastingStatus.coding.code contains '{{C}}')", - ], - [ - "BBMRI_samplingDate", - "exists from [Specimen] S\nwhere FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}}", - ], - [ - "BBMRI_fastingStatus", - "exists from [Specimen] S\nwhere S.collection.fastingStatus.coding.code contains '{{C}}'", - ], - [ - "BBMRI_storageTemperature", - "exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding contains Code '{{C}}' from {{A1}})", - ], + ["retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"], ]); export const criterionMap = new Map( diff --git a/packages/lib/src/types/ast.ts b/packages/lib/src/types/ast.ts index dd3f1b7d..cae1a9b7 100644 --- a/packages/lib/src/types/ast.ts +++ b/packages/lib/src/types/ast.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export type AstElement = AstTopLayer | AstBottomLayerValue; export type AstTopLayer = { @@ -16,3 +17,57 @@ export type AstBottomLayerValue = { | { min: number; max: number } | { min: string; max: string }; // for dates }; +// Type Guards + +/** + * Checks if the given object conforms to the AstBottomLayerValue type. + * @param obj - The object to check. + * @returns True if the object matches the AstBottomLayerValue type, otherwise false. + */ +export function isAstBottomLayerValue(obj: any): obj is AstBottomLayerValue { + return ( + typeof obj === "object" && + obj !== null && + typeof obj.key === "string" && + typeof obj.type === "string" && + (obj.system === undefined || typeof obj.system === "string") && + (typeof obj.value === "string" || + typeof obj.value === "boolean" || + (Array.isArray(obj.value) && + obj.value.every((v) => typeof v === "string")) || + (typeof obj.value === "object" && + obj.value !== null && + (("min" in obj.value && + typeof obj.value.min === "number" && + "max" in obj.value && + typeof obj.value.max === "number") || + ("min" in obj.value && + typeof obj.value.min === "string" && + "max" in obj.value && + typeof obj.value.max === "string")))) + ); +} + +/** + * Checks if the given object conforms to the AstTopLayer type. + * @param obj - The object to check. + * @returns True if the object matches the AstTopLayer type, otherwise false. + */ +export function isAstTopLayer(obj: any): obj is AstTopLayer { + return ( + typeof obj === "object" && + obj !== null && + (obj.operand === "AND" || obj.operand === "OR") && + Array.isArray(obj.children) && + obj.children.every(isAstElement) // Uses isAstElement for each child + ); +} + +/** + * Checks if the given object conforms to the AstElement type, which can be either AstTopLayer or AstBottomLayerValue. + * @param obj - The object to check. + * @returns True if the object matches either AstTopLayer or AstBottomLayerValue type, otherwise false. + */ +export function isAstElement(obj: any): obj is AstElement { + return isAstTopLayer(obj) || isAstBottomLayerValue(obj); +} From 6014c15a4816862b36204f4a6a7127f2d8115726 Mon Sep 17 00:00:00 2001 From: Patrick Skowronek Date: Mon, 20 Jan 2025 11:08:43 +0100 Subject: [PATCH 2/4] feat: added histo counting related to ffpe samples --- packages/demo/src/measures.ts | 2 +- .../ast-to-cql-translator.ts | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/demo/src/measures.ts b/packages/demo/src/measures.ts index 16473add..31c871cc 100644 --- a/packages/demo/src/measures.ts +++ b/packages/demo/src/measures.ts @@ -1088,6 +1088,6 @@ export const dktkHistologyMeasure = { ], }, cql: ` - DKTK_STRAT_HISTOLOGY_STRATIFIER + DKTK_REPLACE_HISTOLOGY_STRATIFIER `, }; diff --git a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts index 1ffd7b90..30b04d52 100644 --- a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts +++ b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts @@ -74,6 +74,22 @@ export const translateAstToCql = ( specimenMeasure.cql = specimenMeasure?.cql + retrievalCriteria; } + const histoMeasure = measures.find((element) => element.key == "Histo"); + if (histoMeasure?.cql) { + if ( + !additionalCriteria.includes("type") || + additionalCriteria.includes("tumor-tissue-ffpe") + ) { + histoMeasure.cql = + histoMeasure.cql + + " if histo.code.coding.where(code = '59847-4').code.first() is null then 0 else 1\n"; + } else { + histoMeasure.cql = + histoMeasure.cql + + " if histo.code.coding.where(code = '59847-4').code.first() is null then 0 else 0\n"; + } + } + if (query.children.length == 0) { singletons += "\ntrue"; } @@ -82,16 +98,6 @@ export const translateAstToCql = ( return singletons; } - console.log( - cqlHeader + - getCodesystems() + - "context Patient\n" + - measures - .map((measureItem: MeasureItem) => measureItem.cql) - .join("") + - singletons, - ); - return ( cqlHeader + getCodesystems() + From d4a5b589f3a7df81f36ca4b48eab43d6b5601fc6 Mon Sep 17 00:00:00 2001 From: Patrick Skowronek Date: Tue, 28 Jan 2025 08:07:31 +0100 Subject: [PATCH 3/4] fix: measure write protection --- .../ast-to-cql-translator.ts | 32 +++++++++++++------ packages/lib/src/types/backend.ts | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts index 254ecbbb..9a9c14ad 100644 --- a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts +++ b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts @@ -18,7 +18,7 @@ import { getCriteria, resolveAstSubCatagories } from "../stores/catalogue"; import type { MeasureItem } from "../types/backend"; let codesystems: string[] = []; -let criteria: string[]; +let criteria: string[] = []; export const translateAstToCql = ( query: AstTopLayer, @@ -26,9 +26,19 @@ export const translateAstToCql = ( backendMeasures: string, measures: MeasureItem[], ): string => { - criteria = getCriteria("diagnosis"); + if (criteria.length == 0) { + criteria = getCriteria("diagnosis"); + codesystems = ["codesystem loinc: 'http://loinc.org'"]; + } - codesystems = ["codesystem loinc: 'http://loinc.org'"]; + const localMeasures: MeasureItem[] = []; + measures.forEach((x) => { + localMeasures.push({ + key: x.key, + measure: undefined, + cql: x.cql, + }); + }); const cqlHeader = "library Retrieve\n" + @@ -63,14 +73,16 @@ export const translateAstToCql = ( } retrievalCriteria = retrievalCriteria += " else {} as List"; - const specimenMeasure = measures.find( + const specimenMeasure = localMeasures.find( (element) => element.key == "specimen", ); - if (specimenMeasure?.cql) { - specimenMeasure.cql = specimenMeasure?.cql + retrievalCriteria; + if (specimenMeasure?.key) { + specimenMeasure.cql = specimenMeasure.cql + retrievalCriteria; } - const histoMeasure = measures.find((element) => element.key == "Histo"); + const histoMeasure = localMeasures.find( + (element) => element.key == "Histo", + ); if (histoMeasure?.cql) { if ( !additionalCriteria.includes("type") || @@ -98,7 +110,9 @@ export const translateAstToCql = ( cqlHeader + getCodesystems() + "context Patient\n" + - measures.map((measureItem: MeasureItem) => measureItem.cql).join("") + + localMeasures + .map((measureItem: MeasureItem) => measureItem.cql) + .join("") + singletons ); }; @@ -127,7 +141,7 @@ const getRetrievalCriterion = (criterion: AstBottomLayerValue): string => { switch (myCriterion.type) { case "specimen": { expression += "("; - myCQL += cqltemplate.get("retrieveSpecimenByType"); + myCQL += cqltemplate.get("specimen"); if (typeof criterion.value === "string") { if (criterion.value.slice(-1) === "%") { const mykey = criterion.value.slice(0, -2); diff --git a/packages/lib/src/types/backend.ts b/packages/lib/src/types/backend.ts index 4c739b07..b071b446 100644 --- a/packages/lib/src/types/backend.ts +++ b/packages/lib/src/types/backend.ts @@ -10,7 +10,7 @@ export type MeasureGroup = { export type MeasureItem = { key: string; - measure: Measure; + measure: Measure | undefined; cql: string; }; export type Measure = { From db1d042cd0aaefb2b2862037ebdf638eab88c24d Mon Sep 17 00:00:00 2001 From: Patrick Skowronek Date: Tue, 28 Jan 2025 09:49:20 +0100 Subject: [PATCH 4/4] fix: use proper cql query and if a accumulatedValues has no values not display it --- .../results/ChartComponent.wc.svelte | 25 +++++++++++-------- .../ast-to-cql-translator.ts | 2 +- .../cqlquery-mappings.ts | 3 ++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/lib/src/components/results/ChartComponent.wc.svelte b/packages/lib/src/components/results/ChartComponent.wc.svelte index 5d7b37d3..726f0a81 100644 --- a/packages/lib/src/components/results/ChartComponent.wc.svelte +++ b/packages/lib/src/components/results/ChartComponent.wc.svelte @@ -271,16 +271,21 @@ valueToAccumulate.values, catalogueGroupCode, ); - - combinedSubGroupData.data.push(aggregationCount); - combinedSubGroupData.labels.push(valueToAccumulate.name); - - for (let i = 0; i < combinedSubGroupData.labels.length; i++) { - const element: string = combinedSubGroupData.labels[i]; - if (valueToAccumulate.values.includes(element)) { - combinedSubGroupData.labels.splice(i, 1); - combinedSubGroupData.data.splice(i, 1); - i--; + if (aggregationCount > 0) { + combinedSubGroupData.data.push(aggregationCount); + combinedSubGroupData.labels.push(valueToAccumulate.name); + + for ( + let i = 0; + i < combinedSubGroupData.labels.length; + i++ + ) { + const element: string = combinedSubGroupData.labels[i]; + if (valueToAccumulate.values.includes(element)) { + combinedSubGroupData.labels.splice(i, 1); + combinedSubGroupData.data.splice(i, 1); + i--; + } } } }); diff --git a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts index 9a9c14ad..6e52e0c4 100644 --- a/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts +++ b/packages/lib/src/cql-translator-service/ast-to-cql-translator.ts @@ -141,7 +141,7 @@ const getRetrievalCriterion = (criterion: AstBottomLayerValue): string => { switch (myCriterion.type) { case "specimen": { expression += "("; - myCQL += cqltemplate.get("specimen"); + myCQL += cqltemplate.get("retrieveSpecimenByType"); if (typeof criterion.value === "string") { if (criterion.value.slice(-1) === "%") { const mykey = criterion.value.slice(0, -2); diff --git a/packages/lib/src/cql-translator-service/cqlquery-mappings.ts b/packages/lib/src/cql-translator-service/cqlquery-mappings.ts index ea86ebb2..bc739421 100644 --- a/packages/lib/src/cql-translator-service/cqlquery-mappings.ts +++ b/packages/lib/src/cql-translator-service/cqlquery-mappings.ts @@ -190,6 +190,7 @@ export const cqltemplate = new Map([ ], ["hasSpecimen", "exists [Specimen]"], ["specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], + ["retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"], [ "TNMc", "exists from [Observation: Code '21908-9' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", @@ -210,7 +211,7 @@ export const cqltemplate = new Map([ "uiccstadium", "(exists ([Observation: Code '21908-9' from loinc] O where O.value.coding.code contains '{{C}}')) or (exists ([Observation: Code '21902-2' from loinc] O where O.value.coding.code contains '{{C}}'))", ], - ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"] + ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"], ]); export const criterionMap = new Map(