From d6db41b3348738f22431f863634767a7eb4bdf1d Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Tue, 4 Feb 2025 11:24:07 +0100 Subject: [PATCH 01/29] chore: remove problematic CQL line from patientsMeasure --- packages/demo/src/measures.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/demo/src/measures.ts b/packages/demo/src/measures.ts index 31c871c..6bbe05d 100644 --- a/packages/demo/src/measures.ts +++ b/packages/demo/src/measures.ts @@ -64,7 +64,6 @@ First( define AgeClass: if (Patient.birthDate is null) then 'unknown' else ToString((AgeInYears() div 10) * 10) -if (PrimaryDiagnosis.onset is null) then 'unknown' else ToString((AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) div 10) * 10) define PatientDeceased: First (from [Observation: Code '75186-7' from loinc] O return O.value.coding.where(system = 'http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VitalstatusCS').code.first()) From b9f497ecf741c837fe31c9d08ae0252ba431c351 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Wed, 5 Feb 2025 09:52:18 +0100 Subject: [PATCH 02/29] chore: fix vite warning --- packages/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/index.ts b/packages/lib/index.ts index e1e982b..3dea685 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -13,4 +13,4 @@ export { default as DataPasser } from "./src/components/DataPasser.wc.svelte"; export { default as ModifiedSearchComponent } from "./src/components/informational/ModifiedSearchComponent.wc.svelte"; export { default as ErrorToasts } from "./src/components/ErrorToasts.wc.svelte"; -export * from "./src/styles/index.css"; +import "./src/styles/index.css"; From e0e4b6dd411dd30e473cabdae82911cd0c45e6f1 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 7 Feb 2025 11:23:43 +0100 Subject: [PATCH 03/29] fix: don't show beam error to user We used to show an error to the user when the "error" event of the server sent events source fires. The error message was misleading because it indicated that the error originated from beam, whereas the "error" event only indicates that the server sent events stream was interrupted. When the stream is interrupted the server sent events source tries to reconnect after firing the "error" event. In our case where the server sents events connection is rather short lived (only until we got results from all sites) it makes sense to treat an interruption of the stream as indication that the server closed the connection and not try to reconnect. In fact, there was already a second event handler for the "error" event that did exactly that. So this commit removes the first event handler that shows the error to the user and keeps the second as it was. --- packages/lib/src/classes/spot.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts index 23fffdc..c15d59b 100644 --- a/packages/lib/src/classes/spot.ts +++ b/packages/lib/src/classes/spot.ts @@ -80,13 +80,6 @@ export class Spot { updateResponse(parsedResponse); }); - // read error events from beam - eventSource.addEventListener("error", (message) => { - console.error(`Beam returned error ${message}`); - errorChannel.set("Fehler von Beam erhalten"); // show user-facing error - eventSource.close(); - }); - // event source in javascript throws an error then the event source is closed by backend eventSource.onerror = () => { console.info( From 1a1df89ba81943c37a8b839b3792072fa6f3b6ca Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 7 Feb 2025 12:17:56 +0100 Subject: [PATCH 04/29] chore: restore old structure of the empty query --- packages/lib/src/helpers/ast-transformer.ts | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/helpers/ast-transformer.ts b/packages/lib/src/helpers/ast-transformer.ts index ba7080c..bc4771a 100644 --- a/packages/lib/src/helpers/ast-transformer.ts +++ b/packages/lib/src/helpers/ast-transformer.ts @@ -21,7 +21,28 @@ import { get } from "svelte/store"; * @returns Ast: the AST will later be converted to a query language of choice */ export const buildAstFromQuery = (queryStore: QueryItem[][]): AstTopLayer => { - return returnNestedValues(queryStore) as AstTopLayer; + const ast = returnNestedValues(queryStore) as AstTopLayer; + + // The empty query is currently a special case because focus and potentially other consumers want it like this + // Instead of: + // {"nodeType":"branch","operand":"OR","children":[{"nodeType":"branch","operand":"AND","children":[]}]} + // We return: + // {"nodeType":"branch","operand":"OR","children":[]} + if (ast.children.length === 1) { + const onlyChild = ast.children[0]; + if ( + onlyChild.nodeType === "branch" && + onlyChild.children.length === 0 + ) { + return { + nodeType: "branch", + operand: "OR", + children: [], + }; + } + } + + return ast; }; /** From 4d0409099c736f20fb36e7c4f8b565e9df7a988b Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 7 Feb 2025 13:32:52 +0100 Subject: [PATCH 05/29] fix: fix that the query modified dialog was not shown when clearing the search bar --- .../src/components/buttons/StoreDeleteButtonComponent.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lib/src/components/buttons/StoreDeleteButtonComponent.svelte b/packages/lib/src/components/buttons/StoreDeleteButtonComponent.svelte index 7ec4a18..e2bab60 100644 --- a/packages/lib/src/components/buttons/StoreDeleteButtonComponent.svelte +++ b/packages/lib/src/components/buttons/StoreDeleteButtonComponent.svelte @@ -3,6 +3,7 @@ queryStore, removeItemFromQuery, removeValueFromQuery, + queryModified, } from "../../stores/query"; import { iconStore } from "../../stores/icons"; import type { QueryItem } from "../../types/queryData"; @@ -23,6 +24,7 @@ dispatch("clear-search"); if (type === "group") { + queryModified.set(true); queryStore.update((query) => { query = query.filter((group, i) => i !== index); if (query.length === 0) { From 2f69fc9eb93dabac90021205029021eaf7ef3828 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Tue, 11 Feb 2025 14:17:51 +0100 Subject: [PATCH 06/29] fix: add missing fields "site_id" and "collection_name" to negotiator options in options schema --- packages/lib/src/types/options.schema.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/lib/src/types/options.schema.json b/packages/lib/src/types/options.schema.json index 43d50bf..9f45b25 100644 --- a/packages/lib/src/types/options.schema.json +++ b/packages/lib/src/types/options.schema.json @@ -355,6 +355,14 @@ "collection": { "type": "string", "pattern": "^.+$" + }, + "site_id": { + "type": "string", + "pattern": "^.+$" + }, + "collection_name": { + "type": "string", + "pattern": "^.+$" } }, "additionalProperties": false, From 42fe9310f2147554ca4b94b7473bebbc92ca4dd9 Mon Sep 17 00:00:00 2001 From: Patrick Skowronek Date: Tue, 11 Feb 2025 16:45:36 +0100 Subject: [PATCH 07/29] feat: also replace % in aggregatedValues --- .../public/catalogues/catalogue-dktk.json | 714 +++++++++--------- .../ast-to-cql-translator.ts | 52 +- packages/lib/src/stores/catalogue.ts | 31 + 3 files changed, 395 insertions(+), 402 deletions(-) diff --git a/packages/demo/public/catalogues/catalogue-dktk.json b/packages/demo/public/catalogues/catalogue-dktk.json index 95e1a37..01ec73f 100644 --- a/packages/demo/public/catalogues/catalogue-dktk.json +++ b/packages/demo/public/catalogues/catalogue-dktk.json @@ -629,7 +629,7 @@ }, { "value": "diagnosis", - "name": "C08.%" + "name": "C08" } ], [ @@ -703,7 +703,7 @@ }, { "value": "diagnosis", - "name": "C08.%" + "name": "C08" } ], [ @@ -8389,7 +8389,7 @@ { "key": "C43-C44", "name": "C43-C44", - "description": "Search for all subgroups of C43,C44", + "description": "Search for all subgroups of C43,C44", "subgroup": [ { "key": "C43.%", @@ -8667,7 +8667,8 @@ "description": "Bösartige Neubildung: Periphere Nerven und autonomes Nervensystem, nicht näher bezeichnet" } ] - }, { + }, + { "key": "C48.%", "name": "C48.%", "description": "Bösartige Neubildung: Näher bezeichnete Teile des Peritoneums", @@ -8699,7 +8700,8 @@ "description": "Bösartige Neubildung: Retroperitoneum und Peritoneum, mehrere Teilbereiche überlappend" } ] - }, { + }, + { "key": "C49.%", "name": "C49.%", "description": "Bösartige Neubildung: Bindegewebe und andere Weichteilgewebe der oberen Extremität, einschließlich Schulter", @@ -11184,7 +11186,8 @@ "description": "Carcinoma in situ: Sonstige und nicht näher bezeichnete Harnorgane" } ] - }] + } + ] }, { "key": "D10-D36", @@ -11923,72 +11926,72 @@ "name": "D24", "description": "Gutartige Neubildung der Brustdrüse [Mamma]" }, - { - "key": "D25.%", - "name": "D25.%", - "description": "Intramurales Leiomyom des Uterus", - "subgroup": [ - { - "visible": false, - "key": "D25", - "name": "D25", - "description": "" - }, - { - "key": "D25.1", - "name": "D25.1", - "description": "Intramurales Leiomyom des Uterus" - }, - { - "key": "D25.9", - "name": "D25.9", - "description": "Leiomyom des Uterus, nicht näher bezeichnet" - }, - { - "key": "D25.0", - "name": "D25.0", - "description": "Submuköses Leiomyom des Uterus" - }, { - "key": "D25.2", - "name": "D25.2", - "description": "Subseröses Leiomyom des Uterus" - } - ] - }, - { - "key": "D26.%", - "name": "D26.%", - "description": "Sonstige gutartige Neubildungen: Cervix uteri", - "subgroup": [ - { - "visible": false, - "key": "D26", - "name": "D26", - "description": "" - }, - { - "key": "D26.0", - "name": "D26.0", - "description": "Sonstige gutartige Neubildungen: Cervix uteri" - }, - { - "key": "D26.1", - "name": "D26.1", - "description": "Sonstige gutartige Neubildungen: Corpus uteri" + "key": "D25.%", + "name": "D25.%", + "description": "Intramurales Leiomyom des Uterus", + "subgroup": [ + { + "visible": false, + "key": "D25", + "name": "D25", + "description": "" + }, + { + "key": "D25.1", + "name": "D25.1", + "description": "Intramurales Leiomyom des Uterus" + }, + { + "key": "D25.9", + "name": "D25.9", + "description": "Leiomyom des Uterus, nicht näher bezeichnet" + }, + { + "key": "D25.0", + "name": "D25.0", + "description": "Submuköses Leiomyom des Uterus" + }, + { + "key": "D25.2", + "name": "D25.2", + "description": "Subseröses Leiomyom des Uterus" + } + ] }, { - "key": "D26.7", - "name": "D26.7", - "description": "Sonstige gutartige Neubildungen: Sonstige Teile des Uterus" + "key": "D26.%", + "name": "D26.%", + "description": "Sonstige gutartige Neubildungen: Cervix uteri", + "subgroup": [ + { + "visible": false, + "key": "D26", + "name": "D26", + "description": "" + }, + { + "key": "D26.0", + "name": "D26.0", + "description": "Sonstige gutartige Neubildungen: Cervix uteri" + }, + { + "key": "D26.1", + "name": "D26.1", + "description": "Sonstige gutartige Neubildungen: Corpus uteri" + }, + { + "key": "D26.7", + "name": "D26.7", + "description": "Sonstige gutartige Neubildungen: Sonstige Teile des Uterus" + }, + { + "key": "D26.9", + "name": "D26.9", + "description": "Sonstige gutartige Neubildungen: Uterus, nicht näher bezeichnet" + } + ] }, - { - "key": "D26.9", - "name": "D26.9", - "description": "Sonstige gutartige Neubildungen: Uterus, nicht näher bezeichnet" - } - ] - }, { "key": "D27", "name": "D27", @@ -12364,190 +12367,189 @@ "key": "D37-D48", "name": "D37-D48", "description": "Search for all subgroups of D37,D38,D39,D40,D41,D42,D43,D44,D45,D46,D47,D48", - "subgroup": [ - - { - "key": "D37.%", - "name": "D37.%", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Appendix vermiformis", "subgroup": [ { - "visible": false, - "key": "D37", - "name": "D37", - "description": "" - }, - { - "key": "D37.3", - "name": "D37.3", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Appendix vermiformis" - }, - { - "key": "D37.2", - "name": "D37.2", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Dünndarm" - }, - { - "key": "D37.4", - "name": "D37.4", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Kolon" - }, - { - "key": "D37.6", - "name": "D37.6", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Leber, Gallenblase und Gallengänge" - }, - { - "key": "D37.0", - "name": "D37.0", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Lippe, Mundhöhle und Pharynx" - }, - { - "key": "D37.1", - "name": "D37.1", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Magen" - }, - { - "key": "D37.5", - "name": "D37.5", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Rektum" - }, - { - "key": "D37.70", - "name": "D37.70", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Pankreas" - }, - { - "key": "D37.78", - "name": "D37.78", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige Verdauungsorgane" - }, - { - "key": "D37.9", - "name": "D37.9", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Verdauungsorgan, nicht näher bezeichnet" - } - ] - }, - { - "key": "D38.%", - "name": "D38.%", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Atmungsorgan, nicht näher bezeichnet", - "subgroup": [ - { - "visible": false, - "key": "D38", - "name": "D38", - "description": "" - }, - { - "key": "D38.6", - "name": "D38.6", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Atmungsorgan, nicht näher bezeichnet" - }, - { - "key": "D38.0", - "name": "D38.0", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Larynx" - }, - { - "key": "D38.3", - "name": "D38.3", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Mediastinum" - }, - { - "key": "D38.2", - "name": "D38.2", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Pleura" - }, - { - "key": "D38.5", - "name": "D38.5", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige Atmungsorgane" - }, - { - "key": "D38.4", - "name": "D38.4", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Thymus" - }, - { - "key": "D38.1", - "name": "D38.1", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Trachea, Bronchus und Lunge" - } - ] - }, - { - "key": "D39.%", - "name": "D39.%", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Ovar", - "subgroup": [ - { - "visible": false, - "key": "D39", - "name": "D39", - "description": "" - }, - { - "key": "D39.1", - "name": "D39.1", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Ovar" - }, - { - "key": "D39.2", - "name": "D39.2", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Plazenta" - }, - { - "key": "D39.7", - "name": "D39.7", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige weibliche Genitalorgane" - }, - { - "key": "D39.0", - "name": "D39.0", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Uterus" - }, - { - "key": "D39.9", - "name": "D39.9", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Weibliches Genitalorgan, nicht näher bezeichnet" - } - ] - }, - { - "key": "D40.%", - "name": "D40.%", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Hoden", - "subgroup": [ - { - "visible": false, - "key": "D40", - "name": "D40", - "description": "" + "key": "D37.%", + "name": "D37.%", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Appendix vermiformis", + "subgroup": [ + { + "visible": false, + "key": "D37", + "name": "D37", + "description": "" + }, + { + "key": "D37.3", + "name": "D37.3", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Appendix vermiformis" + }, + { + "key": "D37.2", + "name": "D37.2", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Dünndarm" + }, + { + "key": "D37.4", + "name": "D37.4", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Kolon" + }, + { + "key": "D37.6", + "name": "D37.6", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Leber, Gallenblase und Gallengänge" + }, + { + "key": "D37.0", + "name": "D37.0", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Lippe, Mundhöhle und Pharynx" + }, + { + "key": "D37.1", + "name": "D37.1", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Magen" + }, + { + "key": "D37.5", + "name": "D37.5", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Rektum" + }, + { + "key": "D37.70", + "name": "D37.70", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Pankreas" + }, + { + "key": "D37.78", + "name": "D37.78", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige Verdauungsorgane" + }, + { + "key": "D37.9", + "name": "D37.9", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Verdauungsorgan, nicht näher bezeichnet" + } + ] }, { - "key": "D40.1", - "name": "D40.1", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Hoden" + "key": "D38.%", + "name": "D38.%", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Atmungsorgan, nicht näher bezeichnet", + "subgroup": [ + { + "visible": false, + "key": "D38", + "name": "D38", + "description": "" + }, + { + "key": "D38.6", + "name": "D38.6", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Atmungsorgan, nicht näher bezeichnet" + }, + { + "key": "D38.0", + "name": "D38.0", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Larynx" + }, + { + "key": "D38.3", + "name": "D38.3", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Mediastinum" + }, + { + "key": "D38.2", + "name": "D38.2", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Pleura" + }, + { + "key": "D38.5", + "name": "D38.5", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige Atmungsorgane" + }, + { + "key": "D38.4", + "name": "D38.4", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Thymus" + }, + { + "key": "D38.1", + "name": "D38.1", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Trachea, Bronchus und Lunge" + } + ] }, { - "key": "D40.9", - "name": "D40.9", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Männliches Genitalorgan, nicht näher bezeichnet" + "key": "D39.%", + "name": "D39.%", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Ovar", + "subgroup": [ + { + "visible": false, + "key": "D39", + "name": "D39", + "description": "" + }, + { + "key": "D39.1", + "name": "D39.1", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Ovar" + }, + { + "key": "D39.2", + "name": "D39.2", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Plazenta" + }, + { + "key": "D39.7", + "name": "D39.7", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige weibliche Genitalorgane" + }, + { + "key": "D39.0", + "name": "D39.0", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Uterus" + }, + { + "key": "D39.9", + "name": "D39.9", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Weibliches Genitalorgan, nicht näher bezeichnet" + } + ] }, { - "key": "D40.0", - "name": "D40.0", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Prostata" + "key": "D40.%", + "name": "D40.%", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Hoden", + "subgroup": [ + { + "visible": false, + "key": "D40", + "name": "D40", + "description": "" + }, + { + "key": "D40.1", + "name": "D40.1", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Hoden" + }, + { + "key": "D40.9", + "name": "D40.9", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Männliches Genitalorgan, nicht näher bezeichnet" + }, + { + "key": "D40.0", + "name": "D40.0", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Prostata" + }, + { + "key": "D40.7", + "name": "D40.7", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige männliche Genitalorgane" + } + ] }, - { - "key": "D40.7", - "name": "D40.7", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige männliche Genitalorgane" - } - ] - }, { "key": "D41.%", "name": "D41.%", @@ -12792,120 +12794,120 @@ "description": "Sonstige myelodysplastische Syndrome" } ] - }, - { - "key": "D47.%", - "name": "D47.%", - "description": "Chronische Eosinophilen-Leukämie [Hypereosinophiles Syndrom]", - "subgroup": [ - { - "visible": false, - "key": "D47", - "name": "D47", - "description": "" - }, - { - "key": "D47.5", - "name": "D47.5", - "description": "Chronische Eosinophilen-Leukämie [Hypereosinophiles Syndrom]" - }, - { - "key": "D47.1", - "name": "D47.1", - "description": "Chronische myeloproliferative Krankheit" - }, - { - "key": "D47.3", - "name": "D47.3", - "description": "Essentielle (hämorrhagische) Thrombozythämie" - }, - { - "key": "D47.0", - "name": "D47.0", - "description": "Histiozyten- und Mastzelltumor unsicheren oder unbekannten Verhaltens" - }, - { - "key": "D47.2", - "name": "D47.2", - "description": "Monoklonale Gammopathie unbestimmter Signifikanz [MGUS]" - }, - { - "key": "D47.9", - "name": "D47.9", - "description": "Neubildung unsicheren oder unbekannten Verhaltens des lymphatischen, blutbildenden und verwandten Gewebes, nicht näher bezeichnet" - }, - { - "key": "D47.4", - "name": "D47.4", - "description": "Osteomyelofibrose" - }, - { - "key": "D47.7", - "name": "D47.7", - "description": "Sonstige näher bezeichnete Neubildungen unsicheren oder unbekannten Verhaltens des lymphatischen, blutbildenden und verwandten Gewebes" - } - ] - }, - { - "key": "D48.%", - "name": "D48.%", - "description": "Neubildung unsicheren oder unbekannten Verhaltens, nicht näher bezeichnet", - "subgroup": [ - { - "visible": false, - "key": "D48", - "name": "D48", - "description": "" - }, - { - "key": "D48.9", - "name": "D48.9", - "description": "Neubildung unsicheren oder unbekannten Verhaltens, nicht näher bezeichnet" - }, - { - "key": "D48.1", - "name": "D48.1", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Bindegewebe und andere Weichteilgewebe" - }, - { - "key": "D48.6", - "name": "D48.6", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Brustdrüse [Mamma]" - }, - { - "key": "D48.5", - "name": "D48.5", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Haut" - }, - { - "key": "D48.0", - "name": "D48.0", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Knochen und Gelenkknorpel" - }, - { - "key": "D48.2", - "name": "D48.2", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Periphere Nerven und autonomes Nervensystem" - }, - { - "key": "D48.4", - "name": "D48.4", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Peritoneum" - }, - { - "key": "D48.3", - "name": "D48.3", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Retroperitoneum" - }, - { - "key": "D48.7", - "name": "D48.7", - "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige näher bezeichnete Lokalisationen" - } - ] - }] + }, + { + "key": "D47.%", + "name": "D47.%", + "description": "Chronische Eosinophilen-Leukämie [Hypereosinophiles Syndrom]", + "subgroup": [ + { + "visible": false, + "key": "D47", + "name": "D47", + "description": "" + }, + { + "key": "D47.5", + "name": "D47.5", + "description": "Chronische Eosinophilen-Leukämie [Hypereosinophiles Syndrom]" + }, + { + "key": "D47.1", + "name": "D47.1", + "description": "Chronische myeloproliferative Krankheit" + }, + { + "key": "D47.3", + "name": "D47.3", + "description": "Essentielle (hämorrhagische) Thrombozythämie" + }, + { + "key": "D47.0", + "name": "D47.0", + "description": "Histiozyten- und Mastzelltumor unsicheren oder unbekannten Verhaltens" + }, + { + "key": "D47.2", + "name": "D47.2", + "description": "Monoklonale Gammopathie unbestimmter Signifikanz [MGUS]" + }, + { + "key": "D47.9", + "name": "D47.9", + "description": "Neubildung unsicheren oder unbekannten Verhaltens des lymphatischen, blutbildenden und verwandten Gewebes, nicht näher bezeichnet" + }, + { + "key": "D47.4", + "name": "D47.4", + "description": "Osteomyelofibrose" + }, + { + "key": "D47.7", + "name": "D47.7", + "description": "Sonstige näher bezeichnete Neubildungen unsicheren oder unbekannten Verhaltens des lymphatischen, blutbildenden und verwandten Gewebes" + } + ] + }, + { + "key": "D48.%", + "name": "D48.%", + "description": "Neubildung unsicheren oder unbekannten Verhaltens, nicht näher bezeichnet", + "subgroup": [ + { + "visible": false, + "key": "D48", + "name": "D48", + "description": "" + }, + { + "key": "D48.9", + "name": "D48.9", + "description": "Neubildung unsicheren oder unbekannten Verhaltens, nicht näher bezeichnet" + }, + { + "key": "D48.1", + "name": "D48.1", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Bindegewebe und andere Weichteilgewebe" + }, + { + "key": "D48.6", + "name": "D48.6", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Brustdrüse [Mamma]" + }, + { + "key": "D48.5", + "name": "D48.5", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Haut" + }, + { + "key": "D48.0", + "name": "D48.0", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Knochen und Gelenkknorpel" + }, + { + "key": "D48.2", + "name": "D48.2", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Periphere Nerven und autonomes Nervensystem" + }, + { + "key": "D48.4", + "name": "D48.4", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Peritoneum" + }, + { + "key": "D48.3", + "name": "D48.3", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Retroperitoneum" + }, + { + "key": "D48.7", + "name": "D48.7", + "description": "Neubildung unsicheren oder unbekannten Verhaltens: Sonstige näher bezeichnete Lokalisationen" + } + ] + } + ] } - ] }, { 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 ea70933..bcfebf6 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 @@ -313,52 +313,12 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { case "observationMolecularMarkerEnsemblID": case "department": { if (typeof criterion.value === "string") { - // TODO: Check if we really need to do this or we can somehow tell cql to do that expansion it self - if ( - criterion.value.slice(-1) === "%" && - criterion.value.length == 5 - ) { - const mykey = criterion.value.slice(0, -2); - if (criteria != undefined) { - const expandedValues = criteria.filter( - (value) => value.startsWith(mykey), - ); - expression += getSingleton({ - nodeType: "leaf", - key: criterion.key, - type: criterion.type, - system: criterion.system, - value: expandedValues, - }); - } - } else if ( - criterion.value.slice(-1) === "%" && - criterion.value.length == 6 - ) { - const mykey = criterion.value.slice(0, -1); - if (criteria != undefined) { - const expandedValues = criteria.filter( - (value) => value.startsWith(mykey), - ); - expandedValues.push( - criterion.value.slice(0, 5), - ); - expression += getSingleton({ - nodeType: "leaf", - key: criterion.key, - type: criterion.type, - system: criterion.system, - value: expandedValues, - }); - } - } else { - expression += substituteCQLExpression( - criterion.key, - myCriterion.alias, - myCQL, - criterion.value as string, - ); - } + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value as string, + ); } if (typeof criterion.value === "boolean") { expression += substituteCQLExpression( diff --git a/packages/lib/src/stores/catalogue.ts b/packages/lib/src/stores/catalogue.ts index 929c163..93e6f9c 100644 --- a/packages/lib/src/stores/catalogue.ts +++ b/packages/lib/src/stores/catalogue.ts @@ -31,6 +31,31 @@ const resolveSubgroupButtoumLayer = (criteria: Criteria[]): string[] => { return collectedCriteria; }; +const resolveSubgroupMatch = ( + key: string, + value: string, + subgroup: Criteria[], +): string[] => { + let newCri: string[] = []; + + for (const cri of subgroup) { + if (cri.key == value) { + if (cri.subgroup instanceof Array) { + newCri = newCri.concat( + resolveSubgroupButtoumLayer(cri.subgroup), + ); + break; + } + } + // Search deeper in the structure for a match + if (cri.subgroup instanceof Array) { + resolveSubgroupMatch(key, value, cri.subgroup); + } + } + + return newCri; +}; + const resolveElementInCatalogueRec = ( key: string, value: string, @@ -49,6 +74,12 @@ const resolveElementInCatalogueRec = ( break; } } + // Search deeper in the structure for a match + if (cri.subgroup instanceof Array) { + newCri = newCri.concat( + resolveSubgroupMatch(key, value, cri.subgroup), + ); + } } } } else { From a5d8942b05f839fb423d62ce4b37057052a9d963 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Tue, 11 Feb 2025 16:51:41 +0100 Subject: [PATCH 08/29] feat: improve BBMRI negotiator error handling and do some cleanup --- packages/lib/src/services/bbmriNegotiate.ts | 217 ++++++++------------ packages/lib/src/types/options.ts | 20 +- 2 files changed, 102 insertions(+), 135 deletions(-) diff --git a/packages/lib/src/services/bbmriNegotiate.ts b/packages/lib/src/services/bbmriNegotiate.ts index 92ab106..52ec21e 100644 --- a/packages/lib/src/services/bbmriNegotiate.ts +++ b/packages/lib/src/services/bbmriNegotiate.ts @@ -1,146 +1,104 @@ +import { get } from "svelte/store"; import { lensOptions } from "../stores/options"; -import type { - LensOptions, - NegotiatorOptions, - NegotiateOptionsSiteMapping, -} from "../types/options"; import { getHumanReadableQuery } from "../stores/datarequests"; import { errorChannel } from "../stores/error-channel"; -type NegotiatorResponse = Response & { - url?: string; - redirect_uri?: string; - id?: string; - status: number; -}; - -let negotiateOptions: NegotiatorOptions; -const siteCollectionMap: Map = new Map(); - -lensOptions.subscribe((options: LensOptions) => { - if (!options) return; - - /** - * TODO: implement multiple collections per site - * need to know how multiple collections are returned from the backend - */ - - negotiateOptions = options.negotiateOptions as NegotiatorOptions; - negotiateOptions?.siteMappings?.forEach((site) => { - siteCollectionMap.set(site.site, site); - }); -}); - /** - * @param sitesToNegotiate the sites to negotiate with - * @returns an array of Collection objects + * The request payload expected by the BBMRI Negotiator to start the application + * process. */ -export const getCollections = ( - sitesToNegotiate: string[], -): NegotiateOptionsSiteMapping[] => { - const siteCollections: NegotiateOptionsSiteMapping[] = []; - - sitesToNegotiate.forEach((site: string) => { - // TODO: Why is site id mapped to Uppercase? - const siteCollection = siteCollectionMap.get(site); - if (siteCollection !== undefined) { - siteCollections.push(siteCollection); - } - }); - - return siteCollections; +type BbmriNegotiateRequest = { + /** URL of the lens interface. The BBMRI Negotiator uses this to send the user back to lens. */ + url: string; + /** A human readable description of the query used in lens */ + humanReadable: string; + /** The resources that are requested */ + resources: BbmriCollectionResource[]; }; /** - * builds a sendable query object from the current query - * sends query to negotiator - * redirects to negotiator - * @param sitesToNegotiate the sites to negotiate with + * Description of a resource as expected by the BBMRI Negotiator. */ -export const bbmrinegotiate = async ( - sitesToNegotiate: string[], -): Promise => { - const humanReadable: string = getHumanReadableQuery(); - const collections: NegotiateOptionsSiteMapping[] = - getCollections(sitesToNegotiate); - const negotiatorResponse = await sendRequestToNegotiator( - humanReadable, - collections, - ); - - switch (negotiatorResponse.status) { - case 201: { - if (!negotiatorResponse.url) { - console.error( - "Negotiator response does not contain redirect uri", - ); - errorChannel.set("Die Antwort vom Negotiator ist fehlerhaft"); // show user-facing error - return; - } else { - const data = await negotiatorResponse.json(); - window.location.href = data.redirectUrl; - } - break; - } - case 401: { - alert( - "An unexpected error has occurred. Please contact support if the issue persists.", - ); - break; - } - case 400: - case 500: { - alert( - "The service is temporarily unavailable. Please try again in a few minutes.", - ); - break; - } - } -}; - -interface BbmriCollectionResource { +type BbmriCollectionResource = { + /** A unique Identifier of the Resource */ id: string; + /** Name of the Resource */ name: string; + /** Organization managing the resource */ organization: { + /** Unique identifier of the organization */ id: number; + /** External identifier of the organization */ externalId: string; + /** Name of the organization */ name: string; }; -} +}; /** - * - * @param humanReadable a human readable query string to view in the negotiator project - * @param collections the collections to negotiate with - * @returns the redirect uri from the negotiator + * Initiate the process of applying for access to samples or data (also known as + * resources). Sends a request to the BBBMRI Negitiator describing the resources + * of interest. The user is then redirected to the BBMRI Negitator to submit the + * application. + * @param sitesToNegotiate The names of the sites with resources of interest. + * These are looked up in the "siteMappings" array of the "negotiateOptions" + * object in the lens options. */ -async function sendRequestToNegotiator( - humanReadable: string, - collections: NegotiateOptionsSiteMapping[], -): Promise { - /** - * handle redirect to negotiator url - */ - const returnURL: string = `${window.location.protocol}//${window.location.host}`; +export async function bbmriNegotiate( + sitesToNegotiate: string[], +): Promise { + const currentLensOptions = get(lensOptions); + if (currentLensOptions.negotiateOptions === undefined) { + console.error('"negotiateOptions" is missing the lens options'); + errorChannel.set('"negotiateOptions" fehlt in den Lens-Optionen'); + return; + } - let response!: Response; + const bbmriCollectionResources: BbmriCollectionResource[] = []; + for (const siteMapping of currentLensOptions.negotiateOptions + .siteMappings) { + if (sitesToNegotiate.includes(siteMapping.site)) { + bbmriCollectionResources.push({ + id: siteMapping.collection, + name: siteMapping.collection, + organization: { + id: 0, + externalId: siteMapping.site_id, + name: siteMapping.site, + }, + }); + } + } - const BBMRI_collection_resource: BbmriCollectionResource[] = []; + const bbmriNegotiatorRequest: BbmriNegotiateRequest = { + humanReadable: getHumanReadableQuery(), + url: `${window.location.protocol}//${window.location.host}`, + resources: bbmriCollectionResources, + }; - collections.forEach(function (collection) { - BBMRI_collection_resource.push({ - id: collection.collection, - name: collection.collection, - organization: { - id: 0, - externalId: collection.site_id, - name: collection.site, - }, - }); - }); + await sendRequestToNegotiator( + currentLensOptions.negotiateOptions.url, + bbmriNegotiatorRequest, + ); +} +/** + * Sends the request to the BBMRI Negotiator to start the application process. + * In case of success the user is redirected to the BBMRI Negotiator to continue + * the application process. In case of a network error or if the BBMRI + * Negotiator responds with an unexpected HTTP status code an error is shown to + * the user. + * @param url The request endpoint for starting the application process + * @param bbmriNegotiateRequest The request payload + */ +async function sendRequestToNegotiator( + url: string, + bbmriNegotiateRequest: BbmriNegotiateRequest, +): Promise { + let response; try { - response = await fetch(`${negotiateOptions.url}`, { + // Swagger documentation: https://negotiator.bbmri-eric.eu/api/swagger-ui/index.html#/Requests/add + response = await fetch(url, { method: "POST", headers: { Accept: "application/json; charset=utf-8", @@ -148,17 +106,22 @@ async function sendRequestToNegotiator( Authorization: "Basic YmJtcmktZGlyZWN0b3J5Omw5RFJLVUROcTBTbDAySXhaUGQ2", }, - body: JSON.stringify({ - humanReadable: humanReadable, - url: returnURL, - resources: BBMRI_collection_resource, - }), + body: JSON.stringify(bbmriNegotiateRequest), }); - - return response as NegotiatorResponse; } catch (error) { console.error(error); - errorChannel.set("Fehler beim Bearbeiten der Anfrage"); // show user-facing error - return new Response() as NegotiatorResponse; + errorChannel.set("Fehler beim Anfragen der Daten und Proben"); + return; + } + + if (response.status === 201) { + const data = await response.json(); + // Redirect the user + window.location.href = data.redirectUrl; + } else { + console.error( + `Expected HTTP status 201 from BBMRI Negotiator but got ${response.status} with response body: ${await response.text()}`, + ); + errorChannel.set("Fehler beim Anfragen der Daten und Proben"); } } diff --git a/packages/lib/src/types/options.ts b/packages/lib/src/types/options.ts index 6b102cc..b88a8b7 100644 --- a/packages/lib/src/types/options.ts +++ b/packages/lib/src/types/options.ts @@ -3,15 +3,24 @@ export type LensOptions = { [key: string]: unknown; chartOptions?: ChartOptions; catalogueKeyToResponseKeyMap?: string[][]; - negotiatorOptions?: NegotiatorOptions; + negotiateOptions?: NegotiateOptions; projectmanagerOptions?: ProjectManagerOptions; }; +export type NegotiateOptions = { + url: string; + siteMappings: NegotiateOptionsSiteMapping[]; +}; + export type NegotiateOptionsSiteMapping = { + /** Name of the site, e.g. "Aachen" */ site: string; - site_id: string; - collection_id: string; + /** Unique identifier of the collection, e.g. "bbmri-eric:ID:DE_RWTHCBMB:collection:RWTHCBMB_BC" */ collection: string; + /** Unique identifier of the site, e.g. "bbmri-eric:ID:DE_RWTHCBMB" */ + site_id: string; + /** Name of the collection, e.g. "Collection of RWTH cBMB Broad Consent Aachen" */ + collection_name: string; }; export type ProjectManagerOptionsSiteMapping = { @@ -19,11 +28,6 @@ export type ProjectManagerOptionsSiteMapping = { collection: string; }; -export type NegotiatorOptions = { - url: string; - siteMappings: NegotiateOptionsSiteMapping[]; -}; - export type ProjectManagerOptions = { newProjectUrl: string; editProjectUrl: string; From d0fb1bfeb0fada7eb1e1a47dbdd83940ad60429c Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Tue, 11 Feb 2025 17:35:53 +0100 Subject: [PATCH 09/29] fix: forgot to include a file in the commit --- .../src/components/buttons/NegotiateButtonComponent.wc.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/buttons/NegotiateButtonComponent.wc.svelte b/packages/lib/src/components/buttons/NegotiateButtonComponent.wc.svelte index f47ba29..5836ce7 100644 --- a/packages/lib/src/components/buttons/NegotiateButtonComponent.wc.svelte +++ b/packages/lib/src/components/buttons/NegotiateButtonComponent.wc.svelte @@ -7,7 +7,7 @@
- {#each toasts as toast (toast.uuid)} -
{toast.message}
+ {#each toasts as [uuid, message] (uuid)} +
+
{message}
+ +
{/each}
diff --git a/packages/lib/src/styles/error-toasts.css b/packages/lib/src/styles/error-toasts.css index a940950..106175c 100644 --- a/packages/lib/src/styles/error-toasts.css +++ b/packages/lib/src/styles/error-toasts.css @@ -13,5 +13,23 @@ error-toasts::part(toast) { border-radius: var(--border-radius-small); background-color: #EF9A9A; border: solid 1px var(--red); + display: flex; + align-items: center; +} + +error-toasts::part(message) { + padding: var(--gap-xs); +} + +error-toasts::part(close-button) { padding: var(--gap-xs); -} \ No newline at end of file + margin-left: auto; /* align right */ + + /* Remove button default styles */ + background: none; + color: inherit; + border: none; + font: inherit; + cursor: pointer; + outline: inherit; +} From fc8680864a3e3dfc3ce92a6fac276084415bd137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20K=C3=B6ther?= Date: Fri, 14 Feb 2025 10:03:11 +0100 Subject: [PATCH 14/29] fix: not clickable links fix --- packages/lib/src/styles/error-toasts.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lib/src/styles/error-toasts.css b/packages/lib/src/styles/error-toasts.css index 106175c..f9425e8 100644 --- a/packages/lib/src/styles/error-toasts.css +++ b/packages/lib/src/styles/error-toasts.css @@ -1,4 +1,5 @@ error-toasts::part(flex-container) { + pointer-events: none; position: fixed; bottom: 0; left: 0; @@ -10,6 +11,7 @@ error-toasts::part(flex-container) { } error-toasts::part(toast) { + pointer-events: auto; border-radius: var(--border-radius-small); background-color: #EF9A9A; border: solid 1px var(--red); From b6dc33cf07a7b7bed0ca20c050aecd6ebc691540 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 14 Feb 2025 11:47:19 +0100 Subject: [PATCH 15/29] feat: add getCatalogueAPI() --- packages/lib/src/components/DataPasser.wc.svelte | 12 +++++++++++- packages/lib/src/types/dataPasser.ts | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/components/DataPasser.wc.svelte b/packages/lib/src/components/DataPasser.wc.svelte index 1befd01..aac7079 100644 --- a/packages/lib/src/components/DataPasser.wc.svelte +++ b/packages/lib/src/components/DataPasser.wc.svelte @@ -6,6 +6,7 @@