From 3f79c54d3909ddab1643fbf3c3a89e4c71f5f148 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 29 Feb 2024 09:37:46 +0100 Subject: [PATCH 01/22] feat(WIP backends): add multiple backends --- packages/demo/public/options.json | 127 ++++++++++++++++- packages/demo/src/AppCCP.svelte | 74 ++-------- packages/demo/src/backends/blaze.ts | 109 +++++++++++++++ packages/demo/src/backends/spot.ts | 128 ++++++++++++++++++ packages/lib/src/classes/spot.ts | 8 +- .../lib/src/components/DataPasser.wc.svelte | 8 ++ packages/lib/src/components/Options.wc.svelte | 7 +- .../buttons/SearchButtonComponenet.wc.svelte | 71 +++++----- packages/lib/src/types/backend.ts | 16 ++- 9 files changed, 440 insertions(+), 108 deletions(-) create mode 100644 packages/demo/src/backends/blaze.ts create mode 100644 packages/demo/src/backends/spot.ts diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index 7dc8d0fa..e9b379ef 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options.json @@ -102,7 +102,7 @@ "rna": "RNA", "derivative-other": "Derivat, Andere" }, - "legendMapping":{ + "legendMapping": { "tissue-ffpe": "Gewebe FFPE", "tissue-frozen": "Gewebe schockgefroren", "tissue-other": "Gewebe, Andere Konservierungsart", @@ -146,8 +146,7 @@ "stratifierCode": "Histlogoies", "stratumCode": "1" }, - { - } + {} ] } ] @@ -165,5 +164,127 @@ "dataKey": "patients" } ] + }, + "backends": { + "spots": [ + { + "url": "http://localhost:8080", + "sites": [ + "berlin", + "berlin-test", + "bonn", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "uiSiteMap": [ + [ + "berlin", + "Berlin" + ], + [ + "berlin-test", + "Berlin Test" + ], + [ + "bonn", + "Bonn" + ], + [ + "dresden", + "Dresden" + ], + [ + "essen", + "Essen" + ], + [ + "frankfurt", + "Frankfurt" + ], + [ + "freiburg", + "Freiburg" + ], + [ + "hannover", + "Hannover" + ], + [ + "mainz", + "Mainz" + ], + [ + "muenchen-lmu", + "München(LMU)" + ], + [ + "muenchen-tum", + "München(TUM)" + ], + [ + "ulm", + "Ulm" + ], + [ + "wuerzburg", + "Würzburg" + ], + [ + "mannheim", + "Mannheim" + ], + [ + "dktk-test", + "DKTK-Test" + ], + [ + "hamburg", + "Hamburg" + ] + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ], + "blazes": [] } } \ No newline at end of file diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index 616df348..41e122d8 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -46,41 +46,11 @@ let catalogueopen = false; - const catalogueKeyToResponseKeyMap = [ - ["gender", "Gender"], - ["age_at_diagnosis", "Age"], - ["diagnosis", "diagnosis"], - ["medicationStatements", "MedicationType"], - ["sample_kind", "sample_kind"], - ["therapy_of_tumor", "ProcedureType"], - ["75186-7", "75186-7"], - // ["encounter", "Encounter"], - ]; - - // VITE_TARGET_ENVIRONMENT should be set by the ci pipeline - const backendUrl = - import.meta.env.VITE_TARGET_ENVIRONMENT === "production" - ? "https://backend.data.dktk.dkfz.de/prod/" - : "https://backend.demo.lens.samply.de/prod/"; - - const uiSiteMap: string[][] = [ - ["berlin", "Berlin"], - ["berlin-test", "Berlin"], - ["bonn", "Bonn"], - ["dresden", "Dresden"], - ["essen", "Essen"], - ["frankfurt", "Frankfurt"], - ["freiburg", "Freiburg"], - ["hannover", "Hannover"], - ["mainz", "Mainz"], - ["muenchen-lmu", "München(LMU)"], - ["muenchen-tum", "München(TUM)"], - ["ulm", "Ulm"], - ["wuerzburg", "Würzburg"], - ["mannheim", "Mannheim"], - ["dktk-test", "DKTK-Test"], - ["hamburg", "Hamburg"], - ]; + // // VITE_TARGET_ENVIRONMENT should be set by the ci pipeline + // const backendUrl = + // import.meta.env.VITE_TARGET_ENVIRONMENT === "production" + // ? "https://backend.data.dktk.dkfz.de/prod/" + // : "https://backend.demo.lens.samply.de/prod/"; const genderHeaders: Map = new Map() .set("male", "männlich") @@ -88,27 +58,6 @@ .set("other", "Divers, Intersexuell") .set("unknown", "unbekannt"); - const backendConfig = { - url: import.meta.env.PROD ? backendUrl : "http://localhost:8080", - backends: [ - "mannheim", - "freiburg", - "muenchen-tum", - "hamburg", - "frankfurt", - "berlin-test", - "dresden", - "mainz", - "muenchen-lmu", - "essen", - "ulm", - "wuerzburg", - "hannover", - ], - uiSiteMap: uiSiteMap, - catalogueKeyToResponseKeyMap: catalogueKeyToResponseKeyMap, - }; - const barChartBackgroundColors: string[] = ["#4dc9f6", "#3da4c7"]; const vitalStateHeaders: Map = new Map() @@ -145,12 +94,7 @@ noQueryMessage="Leere Suchanfrage: Sucht nach allen Ergebnissen." showQuery={true} /> - +
@@ -288,4 +232,8 @@
- + diff --git a/packages/demo/src/backends/blaze.ts b/packages/demo/src/backends/blaze.ts new file mode 100644 index 00000000..a475acd7 --- /dev/null +++ b/packages/demo/src/backends/blaze.ts @@ -0,0 +1,109 @@ +import { buildLibrary, buildMeasure } from "../helpers/cql-measure"; +import { responseStore } from "../stores/response"; +import type { Site } from "../types/response"; +import { measureStore } from "../stores/measures"; + +let measureDefinitions; + +measureStore.subscribe((store) => { + measureDefinitions = store.map((measure) => measure.measure); +}); + +export class Blaze { + constructor( + private url: URL, + private name: string, + private auth: string = "", + ) {} + + /** + * sends the query to beam and updates the store with the results + * @param cql the query as cql string + * @param controller the abort controller to cancel the request + */ + async send(cql: string, controller?: AbortController): Promise { + try { + responseStore.update((store) => { + store.set(this.name, { status: "claimed", data: null }); + return store; + }); + const libraryResponse = await fetch( + new URL(`${this.url}/Library`), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(buildLibrary(cql)), + signal: controller?.signal, + }, + ); + if (!libraryResponse.ok) { + this.handleError( + `Couldn't create Library in Blaze`, + libraryResponse, + ); + } + const library = await libraryResponse.json(); + const measureResponse = await fetch( + new URL(`${this.url}/Measure`), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + buildMeasure(library.url, measureDefinitions), + ), + signal: controller.signal, + }, + ); + if (!measureResponse.ok) { + this.handleError( + `Couldn't create Measure in Blaze`, + measureResponse, + ); + } + const measure = await measureResponse.json(); + const dataResponse = await fetch( + new URL( + `${this.url}/Measure/$evaluate-measure?measure=${measure.url}&periodStart=2000&periodEnd=2030`, + ), + { + signal: controller.signal, + }, + ); + if (!dataResponse.ok) { + this.handleError( + `Couldn't evaluate Measure in Blaze`, + dataResponse, + ); + } + const blazeResponse: Site = await dataResponse.json(); + responseStore.update((store) => { + store.set(this.name, { + status: "succeeded", + data: blazeResponse, + }); + return store; + }); + } catch (err) { + if (err.name === "AbortError") { + console.log(`Aborting former blaze request.`); + } else { + console.error(err); + } + } + } + + async handleError(message: string, response: Response): Promise { + const errorMessage = await response.text(); + console.debug( + `${message}. Received error ${response.status} with message ${errorMessage}`, + ); + responseStore.update((store) => { + store.set(this.name, { status: "permfailed", data: null }); + return store; + }); + } +} diff --git a/packages/demo/src/backends/spot.ts b/packages/demo/src/backends/spot.ts new file mode 100644 index 00000000..f4725106 --- /dev/null +++ b/packages/demo/src/backends/spot.ts @@ -0,0 +1,128 @@ +/** + * TODO: document this class + */ + +import { responseStore } from "../stores/response"; +import type { ResponseStore } from "../types/backend"; + +import type { Site, SiteData, Status } from "../types/response"; + +type BeamResult = { + body: string; + from: string; + metadata: string; + status: Status; + task: string; + to: string[]; +}; + +export class Spot { + private storeCache!: ResponseStore; + private currentTask!: string; + + constructor( + private url: URL, + private sites: Array, + ) { + responseStore.subscribe( + (store: ResponseStore) => (this.storeCache = store), + ); + } + + /** + * sends the query to beam and updates the store with the results + * @param query the query as base64 encoded string + * @param controller the abort controller to cancel the request + */ + async send(query: string, controller?: AbortController): Promise { + try { + const beamTaskResponse = await fetch( + `${this.url}tasks?sites=${this.sites.toString()}`, + { + method: "POST", + credentials: import.meta.env.PROD ? "include" : "omit", + body: query, + signal: controller?.signal, + }, + ); + if (!beamTaskResponse.ok) { + const error = await beamTaskResponse.text(); + console.debug( + `Received ${beamTaskResponse.status} with message ${error}`, + ); + throw new Error(`Unable to create new beam task.`); + } + this.currentTask = (await beamTaskResponse.json()).id; + + let responseCount: number = 0; + + do { + const beamResponses: Response = await fetch( + `${this.url}tasks/${this.currentTask}?wait_count=${responseCount + 1}`, + { + credentials: import.meta.env.PROD ? "include" : "omit", + signal: controller?.signal, + }, + ); + + if (!beamResponses.ok) { + const error: string = await beamResponses.text(); + console.debug( + `Received ${beamResponses.status} with message ${error}`, + ); + throw new Error( + `Error then retrieving responses from Beam. Abborting requests ...`, + ); + } + + const beamResponseData: Array = + await beamResponses.json(); + + const changes = new Map(); + beamResponseData.forEach((response: BeamResult) => { + if (response.task !== this.currentTask) return; + const site: string = response.from.split(".")[1]; + const status: Status = response.status; + const body: SiteData = + status === "succeeded" + ? JSON.parse(atob(response.body)) + : null; + + // if the site is already in the store and the status is claimed, don't update the store + if (this.storeCache.get(site)?.status === status) return; + + changes.set(site, { status: status, data: body }); + }); + if (changes.size > 0) { + responseStore.update( + (store: ResponseStore): ResponseStore => { + changes.forEach((value, key) => { + store.set(key, value); + }); + return store; + }, + ); + } + + responseCount = beamResponseData.length; + const realResponseCount = beamResponseData.filter( + (response) => response.status !== "claimed", + ).length; + + if ( + (beamResponses.status !== 200 && + beamResponses.status !== 206) || + realResponseCount === this.sites.length + ) { + break; + } + } while (true); + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + console.log(`Aborting request ${this.currentTask}`); + } else { + console.error(err); + } + } + } +} diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts index f4725106..62b32d7f 100644 --- a/packages/lib/src/classes/spot.ts +++ b/packages/lib/src/classes/spot.ts @@ -2,7 +2,8 @@ * TODO: document this class */ -import { responseStore } from "../stores/response"; +import type { Writable } from "svelte/store"; +// import { responseStore } from "../stores/response"; import type { ResponseStore } from "../types/backend"; import type { Site, SiteData, Status } from "../types/response"; @@ -23,8 +24,9 @@ export class Spot { constructor( private url: URL, private sites: Array, + private responseStore: Writable = responseStore, ) { - responseStore.subscribe( + this.responseStore.subscribe( (store: ResponseStore) => (this.storeCache = store), ); } @@ -94,7 +96,7 @@ export class Spot { changes.set(site, { status: status, data: body }); }); if (changes.size > 0) { - responseStore.update( + this.responseStore.update( (store: ResponseStore): ResponseStore => { changes.forEach((value, key) => { store.set(key, value); diff --git a/packages/lib/src/components/DataPasser.wc.svelte b/packages/lib/src/components/DataPasser.wc.svelte index 5d9676a4..c9e234cd 100644 --- a/packages/lib/src/components/DataPasser.wc.svelte +++ b/packages/lib/src/components/DataPasser.wc.svelte @@ -113,4 +113,12 @@ export const getAstAPI = (): AstTopLayer => { return buildAstFromQuery($queryStore); }; + + /** + * sets the response from the backend + * @param response the response from the backend + */ + export const setResponseAPI = (response: ResponseStore): void => { + responseStore.set(response); + }; diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index a3db079a..d21185e0 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -15,11 +15,16 @@ */ import { lensOptions } from "../stores/options"; import { catalogue } from "../stores/catalogue"; + import { measureStore } from "../stores/measures"; import type { Criteria } from "../types/treeData"; + import type { Measure } from "../types/backend"; + import type { LensOptions } from "../types/options"; - export let options: object = {}; + export let options: LensOptions = {}; export let catalogueData: Criteria[] = []; + export let measures: Measure[] = []; $: $lensOptions = options; $: $catalogue = catalogueData; + $: $measureStore = measures; diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 46d2866c..6305c6d7 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -20,23 +20,16 @@ catalogueKeyToResponseKeyMap, uiSiteMappingsStore, } from "../../stores/mappings"; - import type { Measure, BackendConfig } from "../../types/backend"; import { responseStore } from "../../stores/response"; + import { lensOptions } from "../../stores/options"; + import type { BackendOptions, SpotOption } from "../../types/backend"; export let title: string = "Search"; - export let backendConfig: BackendConfig = { - url: "http://localhost:8080", - backends: ["dktk-test", "mannheim"], - uiSiteMap: [ - ["dktk-test", "DKTK Test"], - ["mannheim", "Mannheim"], - ], - catalogueKeyToResponseKeyMap: [], - }; export let disabled: boolean = false; - export let measures: Measure[] = []; - export let backendMeasures: string = ""; + + $: options = $lensOptions?.backends as BackendOptions; + let controller: AbortController; /** @@ -45,28 +38,31 @@ * therefore it's a 2d array of strings which is converted to a map */ $: uiSiteMappingsStore.update((mappings) => { - backendConfig.uiSiteMap.forEach((site) => { - mappings.set(site[0], site[1]); + options?.spots.forEach((spot) => { + spot.uiSiteMap.forEach((site) => { + mappings.set(site[0], site[1]); + }); }); return mappings; }); $: catalogueKeyToResponseKeyMap.update((mappings) => { - backendConfig.catalogueKeyToResponseKeyMap.forEach((mapping) => { - mappings.set(mapping[0], mapping[1]); + options?.spots.forEach((spot) => { + spot.catalogueKeyToResponseKeyMap.forEach((mapping) => { + mappings.set(mapping[0], mapping[1]); + }); }); return mappings; }); - /** - * watches the measures for changes to populate the measureStore - */ - $: measureStore.set(measures); - /** * triggers a request to the backend via the spot class */ - const getResultsFromBackend = async (): void => { + const getResultsFromBackend = (): void => { + /** + * TODO emit event + */ + if (controller) { controller.abort(); } @@ -75,22 +71,29 @@ controller = new AbortController(); const ast = buildAstFromQuery($queryStore); - const cql = translateAstToCql(ast, false, backendMeasures); - const library = buildLibrary(`${cql}`); - const measure = buildMeasure( - library.url, - $measureStore.map((measureItem) => measureItem.measure), - ); - const query = { lang: "cql", lib: library, measure: measure }; + options.spots.forEach((spot: SpotOption) => { + /** + * TODO: add backend measures + */ + const cql = translateAstToCql(ast, false, "backend-measures"); - const backend = new Spot( - new URL(backendConfig.url), - backendConfig.backends, - ); + const library = buildLibrary(`${cql}`); + const measure = buildMeasure( + library.url, + $measureStore.map((measureItem) => measureItem.measure), + ); + const query = { lang: "cql", lib: library, measure: measure }; - backend.send(btoa(decodeURI(JSON.stringify(query))), controller); + const backend = new Spot( + new URL(spot.url), + spot.sites, + responseStore, + ); + console.log(backend); + backend.send(btoa(decodeURI(JSON.stringify(query))), controller); + }); queryModified.set(false); }; diff --git a/packages/lib/src/types/backend.ts b/packages/lib/src/types/backend.ts index 62974bc5..31c07692 100644 --- a/packages/lib/src/types/backend.ts +++ b/packages/lib/src/types/backend.ts @@ -6,11 +6,19 @@ export type Measure = { cql: string; }; -export type BackendConfig = { +export type ResponseStore = Map; + +export type SpotOption = { url: string; - backends: string[]; + sites: string[]; uiSiteMap: string[][]; catalogueKeyToResponseKeyMap: string[][]; }; - -export type ResponseStore = Map; +/** + * TODO: implement BlazeOption + */ +export type BlazeOption = null; +export type BackendOptions = { + spots: SpotOption[]; + blazes: BlazeOption[]; +}; From a980e77430118e089628071edc2c1aa81964dd95 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Mon, 4 Mar 2024 11:10:39 +0100 Subject: [PATCH 02/22] feat(measures): add measure configuration for multiple spots --- packages/demo/public/options.json | 2 + packages/demo/src/AppCCP.svelte | 21 ++++---- packages/lib/src/components/Options.wc.svelte | 4 +- .../buttons/SearchButtonComponenet.wc.svelte | 38 ++++++++++---- .../ast-to-cql-translator.ts | 15 ++---- packages/lib/src/helpers/cql-measure.ts | 5 +- packages/lib/src/stores/measures.ts | 8 +-- packages/lib/src/types/backend.ts | 51 ++++++++++++++++++- 8 files changed, 104 insertions(+), 40 deletions(-) diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index e9b379ef..bb97291a 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options.json @@ -168,6 +168,8 @@ "backends": { "spots": [ { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", "url": "http://localhost:8080", "sites": [ "berlin", diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index 41e122d8..7302dd0b 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -24,16 +24,19 @@ }); const measures = [ - dktkPatientsMeasure, - dktkDiagnosisMeasure, - dktkSpecimenMeasure, - dktkProceduresMeasure, - dktkMedicationStatementsMeasure, - dktkHistologyMeasure, + { + name: "DKTK", + measures: [ + dktkPatientsMeasure, + dktkDiagnosisMeasure, + dktkSpecimenMeasure, + dktkProceduresMeasure, + dktkMedicationStatementsMeasure, + dktkHistologyMeasure, + ], + }, ]; - const backendMeasures = `DKTK_STRAT_DEF_IN_INITIAL_POPULATION`; - const catalogueText = { group: "Group", collapseButtonTitle: "Collapse Tree", @@ -94,7 +97,7 @@ noQueryMessage="Leere Suchanfrage: Sucht nach allen Ergebnissen." showQuery={true} /> - +
diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index d21185e0..1d3333e5 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -17,12 +17,12 @@ import { catalogue } from "../stores/catalogue"; import { measureStore } from "../stores/measures"; import type { Criteria } from "../types/treeData"; - import type { Measure } from "../types/backend"; + import type { MeasureStore } from "../types/backend"; import type { LensOptions } from "../types/options"; export let options: LensOptions = {}; export let catalogueData: Criteria[] = []; - export let measures: Measure[] = []; + export let measures: MeasureStore = {} as MeasureStore; $: $lensOptions = options; $: $catalogue = catalogueData; diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 6305c6d7..81055ac7 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -22,7 +22,13 @@ } from "../../stores/mappings"; import { responseStore } from "../../stores/response"; import { lensOptions } from "../../stores/options"; - import type { BackendOptions, SpotOption } from "../../types/backend"; + import type { + BackendOptions, + Measure, + MeasureItem, + MeasureOption, + SpotOption, + } from "../../types/backend"; export let title: string = "Search"; @@ -73,16 +79,30 @@ const ast = buildAstFromQuery($queryStore); options.spots.forEach((spot: SpotOption) => { - /** - * TODO: add backend measures - */ - const cql = translateAstToCql(ast, false, "backend-measures"); + const name = spot.name; + const measureItem: MeasureOption | undefined = $measureStore.find( + (measureStoreItem: MeasureOption) => + spot.name === measureStoreItem.name, + ); - const library = buildLibrary(`${cql}`); - const measure = buildMeasure( - library.url, - $measureStore.map((measureItem) => measureItem.measure), + if (measureItem === undefined) { + throw new Error( + `No measures found for backend ${name}. Please check the measures store.`, + ); + } + const measures: Measure[] = measureItem.measures.map( + (measureItem: MeasureItem) => measureItem.measure, + ); + + const cql = translateAstToCql( + ast, + false, + spot.backendMeasures, + measureItem.measures, ); + + const library = buildLibrary(`${cql}`); + const measure = buildMeasure(library.url, measures); const query = { lang: "cql", lib: library, measure: measure }; const backend = new Spot( 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 494be428..4f8438c5 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 @@ -13,17 +13,7 @@ import { criterionMap, } from "./cqlquery-mappings"; import { getCriteria } from "../stores/catalogue"; -import type { Measure } from "../types/backend"; -import { measureStore } from "../stores/measures"; - -/** - * Get all cql from the project specific measures from the store - */ -let measuresCql: string[] = []; - -measureStore.subscribe((measures: Measure[]) => { - measuresCql = measures.map((measure) => measure.cql); -}); +import type { MeasureItem } from "../types/backend"; let codesystems: string[] = []; let criteria: string[]; @@ -32,6 +22,7 @@ export const translateAstToCql = ( query: AstTopLayer, returnOnlySingeltons: boolean = true, backendMeasures: string, + measures: MeasureItem[], ): string => { criteria = getCriteria("diagnosis"); @@ -66,7 +57,7 @@ export const translateAstToCql = ( cqlHeader + getCodesystems() + "context Patient\n" + - measuresCql.join("") + + measures.map((measureItem: MeasureItem) => measureItem.cql).join("") + singletons ); }; diff --git a/packages/lib/src/helpers/cql-measure.ts b/packages/lib/src/helpers/cql-measure.ts index d93b3c10..21f3b59a 100644 --- a/packages/lib/src/helpers/cql-measure.ts +++ b/packages/lib/src/helpers/cql-measure.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from "uuid"; +import type { Measure } from "../types/backend"; type BuildLibraryReturn = { resourceType: string; @@ -57,12 +58,12 @@ type BuildMeasureReturn = { code: string; }[]; }; - group: object[]; + group: Measure[]; }; export const buildMeasure = ( libraryUrl: string, - measures: object[], + measures: Measure[], ): BuildMeasureReturn => { const measureId = uuidv4(); return { diff --git a/packages/lib/src/stores/measures.ts b/packages/lib/src/stores/measures.ts index b078c7cb..0684b461 100644 --- a/packages/lib/src/stores/measures.ts +++ b/packages/lib/src/stores/measures.ts @@ -1,9 +1,9 @@ -import { writable } from 'svelte/store'; -import type { Measure } from '../types/backend'; +import { writable } from "svelte/store"; +import type { MeasureStore } from "../types/backend"; /** - * Store to hold the measures + * Store to hold the measures * populated by the search button */ -export const measureStore = writable([]); \ No newline at end of file +export const measureStore = writable(); diff --git a/packages/lib/src/types/backend.ts b/packages/lib/src/types/backend.ts index 31c07692..03ea6845 100644 --- a/packages/lib/src/types/backend.ts +++ b/packages/lib/src/types/backend.ts @@ -1,14 +1,61 @@ import type { Site } from "./response"; -export type Measure = { +export type MeasureItem = { key: string; - measure: object; + measure: Measure; cql: string; }; +export type Measure = { + code: { + text: string; + }; + extension: [ + { + url: string; + valueCode: string; + }, + ]; + population: [ + { + code: { + coding: [ + { + system: string; + code: string; + }, + ]; + }; + criteria: { + language: string; + expression: string; + }; + }, + ]; + stratifier: [ + { + code: { + text: string; + }; + criteria: { + language: string; + expression: string; + }; + }, + ]; +}; + +export type MeasureOption = { + name: string; + measures: MeasureItem[]; +}; + +export type MeasureStore = MeasureOption[]; export type ResponseStore = Map; export type SpotOption = { + name: string; + backendMeasures: string; url: string; sites: string[]; uiSiteMap: string[][]; From 06ce3ac11ffd710ed2e88c2e032159a89ab3f4b8 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Wed, 6 Mar 2024 16:13:54 +0100 Subject: [PATCH 03/22] feat(backends): add support for project hosted backends - add additional spot to ccp App - search triggers event which contains store update function and ast --- packages/demo/public/options.json | 45 ++- packages/demo/src/AppCCP.svelte | 64 ++++ .../demo/src/AppFragmentDevelopment.svelte | 5 +- .../src/backends/ast-to-cql-translator.ts | 353 ++++++++++++++++++ packages/demo/src/backends/cql-measure.ts | 92 +++++ .../demo/src/backends/cqlquery-mappings.ts | 313 ++++++++++++++++ packages/demo/src/backends/spot.ts | 49 +-- packages/lib/src/classes/spot.ts | 44 +-- .../lib/src/components/DataPasser.wc.svelte | 21 +- .../buttons/SearchButtonComponenet.wc.svelte | 49 ++- packages/lib/src/stores/catalogue.ts | 2 +- packages/lib/src/stores/query.ts | 7 +- packages/lib/src/stores/response.ts | 25 ++ .../src/{interfaces => types}/DataPasser.d.ts | 10 +- packages/lib/src/types/backend.ts | 9 + packages/lib/src/types/response.ts | 9 + 16 files changed, 998 insertions(+), 99 deletions(-) create mode 100644 packages/demo/src/backends/ast-to-cql-translator.ts create mode 100644 packages/demo/src/backends/cql-measure.ts create mode 100644 packages/demo/src/backends/cqlquery-mappings.ts rename packages/lib/src/{interfaces => types}/DataPasser.d.ts (67%) diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index bb97291a..30c774cb 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options.json @@ -173,7 +173,6 @@ "url": "http://localhost:8080", "sites": [ "berlin", - "berlin-test", "bonn", "dresden", "essen", @@ -285,6 +284,50 @@ "75186-7" ] ] + }, + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "http://localhost:8080", + "sites": [ + "berlin-test" + ], + "uiSiteMap": [ + [ + "berlin-test", + "Berlin Test" + ] + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] } ], "blazes": [] diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index 7302dd0b..837e9618 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -1,4 +1,6 @@
@@ -240,3 +303,4 @@ catalogueData={mockCatalogueData} {measures} /> + diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index b60d82b2..88821a6c 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -1,4 +1,5 @@ diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 81055ac7..8ab7f097 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -20,7 +20,7 @@ catalogueKeyToResponseKeyMap, uiSiteMappingsStore, } from "../../stores/mappings"; - import { responseStore } from "../../stores/response"; + import { responseStore, updateResponseStore } from "../../stores/response"; import { lensOptions } from "../../stores/options"; import type { BackendOptions, @@ -29,6 +29,8 @@ MeasureOption, SpotOption, } from "../../types/backend"; + import type { AstTopLayer } from "../../types/ast"; + import type { Site } from "../../types/response"; export let title: string = "Search"; @@ -62,13 +64,12 @@ }); /** - * triggers a request to the backend via the spot class + * Triggers a request to the backend. + * Multiple spots and blazes can be configured in lens options. + * Emits the ast and the updateResponseStore function to the project + * for running the query on other backends as well. */ const getResultsFromBackend = (): void => { - /** - * TODO emit event - */ - if (controller) { controller.abort(); } @@ -105,17 +106,39 @@ const measure = buildMeasure(library.url, measures); const query = { lang: "cql", lib: library, measure: measure }; - const backend = new Spot( - new URL(spot.url), - spot.sites, - responseStore, - ); - console.log(backend); + const backend = new Spot(new URL(spot.url), spot.sites); - backend.send(btoa(decodeURI(JSON.stringify(query))), controller); + backend.send( + btoa(decodeURI(JSON.stringify(query))), + updateResponseStore, + controller, + ); }); + emitEvent(ast); queryModified.set(false); }; + + interface QueryEvent extends Event { + detail: { + ast: AstTopLayer; + updateResponse: (response: Map) => void; + abortController?: AbortController; + }; + } + /** + * Emits the ast and the updateResponseStore function to the project + * @param ast the ast to be emitted + */ + const emitEvent = (ast: AstTopLayer): void => { + const event: QueryEvent = new CustomEvent("emit-lens-query", { + detail: { + ast: ast, + updateResponse: updateResponseStore, + abortController: controller, + }, + }); + window.dispatchEvent(event); + };
- + + diff --git a/packages/demo/src/backends/ast-to-cql-translator.ts b/packages/demo/src/backends/ast-to-cql-translator.ts index 8cce0085..6b6a8148 100644 --- a/packages/demo/src/backends/ast-to-cql-translator.ts +++ b/packages/demo/src/backends/ast-to-cql-translator.ts @@ -110,8 +110,11 @@ 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": @@ -122,6 +125,8 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { case "procedureResidualstatus": case "medicationStatement": case "specimen": + case "BBMRI_specimen": + case "BBMRI_hasSpecimen": case "hasSpecimen": case "Organization": case "observationMolecularMarkerName": @@ -134,7 +139,10 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { case "TNMc": { 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) === "%") { + if ( + criterion.value.slice(-1) === "%" && + criterion.value.length == 5 + ) { const mykey = criterion.value.slice(0, -2); if (criteria != undefined) { const expandedValues = criteria.filter( @@ -147,6 +155,25 @@ const getSingleton = (criterion: AstBottomLayerValue): string => { 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({ + key: criterion.key, + type: criterion.type, + system: criterion.system, + value: expandedValues, + }); + } } else { expression += substituteCQLExpression( criterion.key, diff --git a/packages/demo/src/backends/cqlquery-mappings.ts b/packages/demo/src/backends/cqlquery-mappings.ts index f6b94452..12bcac63 100644 --- a/packages/demo/src/backends/cqlquery-mappings.ts +++ b/packages/demo/src/backends/cqlquery-mappings.ts @@ -63,6 +63,21 @@ export const alias = new Map([ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMmSymbolCS", ], ["molecularMarker", "http://www.genenames.org"], + + ["BBMRI_icd10", "http://hl7.org/fhir/sid/icd-10"], + ["BBMRI_icd10gm", "http://fhir.de/CodeSystem/dimdi/icd-10-gm"], + [ + "BBMRI_SampleMaterialType", + "https://fhir.bbmri.de/CodeSystem/SampleMaterialType", + ], //specimentype + [ + "BBMRI_StorageTemperature", + "https://fhir.bbmri.de/CodeSystem/StorageTemperature", + ], + [ + "BBMRI_SmokingStatus", + "http://hl7.org/fhir/uv/ips/ValueSet/current-smoking-status-uv-ips", + ], ]); export const cqltemplate = new Map([ @@ -192,6 +207,69 @@ 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( @@ -309,5 +387,43 @@ export const criterionMap = new Map( ["75186-7", { type: "observation", alias: ["loinc", "vitalstatuscs"] }], //Vitalstatus //["Organization", {type: "Organization"}], ["Organization", { type: "department" }], + + ["BBMRI_gender", { type: "BBMRI_gender" }], + [ + "BBMRI_diagnosis", + { + type: "BBMRI_conditionSampleDiagnosis", + alias: ["BBMRI_icd10", "BBMRI_icd10gm"], + }, + ], + [ + "BBMRI_body_weight", + { type: "BBMRI_observationBodyWeight", alias: ["loinc"] }, + ], //Body weight + ["BBMRI_bmi", { type: "BBMRI_observationBMI", alias: ["loinc"] }], //BMI + [ + "BBMRI_smoking_status", + { type: "BBMRI_observationSmoker", alias: ["loinc"] }, + ], //Smoking habit + ["BBMRI_donor_age", { type: "BBMRI_age" }], + ["BBMRI_date_of_diagnosis", { type: "BBMRI_conditionRangeDate" }], + [ + "BBMRI_sample_kind", + { type: "BBMRI_specimen", alias: ["BBMRI_SampleMaterialType"] }, + ], + [ + "BBMRI_storage_temperature", + { + type: "BBMRI_storageTemperature", + alias: ["BBMRI_StorageTemperature"], + }, + ], + ["BBMRI_pat_with_samples", { type: "BBMRI_hasSpecimen" }], + ["BBMRI_diagnosis_age_donor", { type: "BBMRI_conditionRangeAge" }], + [ + "BBMRI_fasting_status", + { type: "BBMRI_fastingStatus", alias: ["loinc"] }, + ], + ["BBMRI_sampling_date", { type: "BBMRI_samplingDate" }], ], ); diff --git a/packages/demo/src/backends/spot.ts b/packages/demo/src/backends/spot.ts index 0956ec7d..fa1f0d38 100644 --- a/packages/demo/src/backends/spot.ts +++ b/packages/demo/src/backends/spot.ts @@ -3,11 +3,10 @@ */ import type { - Site, + ResponseStore, SiteData, Status, BeamResult, - ResponseStore, } from "../../../../dist/types"; export class Spot { @@ -21,22 +20,30 @@ export class Spot { /** * sends the query to beam and updates the store with the results * @param query the query as base64 encoded string - * @param updateResponseStore the function to update the response store + * @param updateResponse the function to update the response store * @param controller the abort controller to cancel the request */ async send( query: string, - updateResponseStore: (response: ResponseStore) => void, - controller?: AbortController, + updateResponse: (response: ResponseStore) => void, + controller: AbortController, ): Promise { try { + this.currentTask = crypto.randomUUID(); const beamTaskResponse = await fetch( - `${this.url}tasks?sites=${this.sites.toString()}`, + `${this.url}beam?sites=${this.sites.toString()}`, { method: "POST", + headers: { + "Content-Type": "application/json", + }, credentials: import.meta.env.PROD ? "include" : "omit", - body: query, - signal: controller?.signal, + body: JSON.stringify({ + id: this.currentTask, + sites: this.sites, + query: query, + }), + signal: controller.signal, }, ); if (!beamTaskResponse.ok) { @@ -46,60 +53,49 @@ export class Spot { ); throw new Error(`Unable to create new beam task.`); } - this.currentTask = (await beamTaskResponse.json()).id; - - let responseCount: number = 0; - - do { - const beamResponses: Response = await fetch( - `${this.url}tasks/${this.currentTask}?wait_count=${responseCount + 1}`, - { - credentials: import.meta.env.PROD ? "include" : "omit", - signal: controller?.signal, - }, - ); - if (!beamResponses.ok) { - const error: string = await beamResponses.text(); - console.debug( - `Received ${beamResponses.status} with message ${error}`, - ); - throw new Error( - `Error then retrieving responses from Beam. Abborting requests ...`, - ); - } + console.info(`Created new Beam Task with id ${this.currentTask}`); - const beamResponseData: Array = - await beamResponses.json(); + const eventSource = new EventSource( + `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`, + { + withCredentials: true, + }, + ); - const changes = new Map(); - beamResponseData.forEach((response: BeamResult) => { - if (response.task !== this.currentTask) return; - const site: string = response.from.split(".")[1]; - const status: Status = response.status; - const body: SiteData = - status === "succeeded" - ? JSON.parse(atob(response.body)) - : null; + /** + * Listenes to the new_result event from beam and updates the response store + */ + eventSource.addEventListener("new_result", (message) => { + const response: BeamResult = JSON.parse(message.data); + if (response.task !== this.currentTask) return; + const site: string = response.from.split(".")[1]; + const status: Status = response.status; + const body: SiteData = + status === "succeeded" + ? JSON.parse(atob(response.body)) + : null; - changes.set(site, { status: status, data: body }); + const parsedResponse: ResponseStore = new Map().set(site, { + status: status, + data: body, }); + updateResponse(parsedResponse); + }); - updateResponseStore(changes); - - responseCount = beamResponseData.length; - const realResponseCount = beamResponseData.filter( - (response) => response.status !== "claimed", - ).length; + // read error events from beam + eventSource.addEventListener("error", (message) => { + console.error(`Beam returned error ${message}`); + eventSource.close(); + }); - if ( - (beamResponses.status !== 200 && - beamResponses.status !== 206) || - realResponseCount === this.sites.length - ) { - break; - } - } while (true); + // event source in javascript throws an error then the event source is closed by backend + eventSource.onerror = () => { + console.info( + `Querying results from sites for task ${this.currentTask} finished.`, + ); + eventSource.close(); + }; } catch (err) { if (err instanceof Error && err.name === "AbortError") { console.log(`Aborting request ${this.currentTask}`); diff --git a/packages/demo/src/fragment-development.css b/packages/demo/src/fragment-development.css index 7ed33451..148490e3 100644 --- a/packages/demo/src/fragment-development.css +++ b/packages/demo/src/fragment-development.css @@ -1,4 +1,4 @@ -@import "../../../node_modules/@samply/lens/dist/style.css"; +/* @import "../../../node_modules/@samply/lens/dist/style.css"; */ @import "../../lib/src/styles/index.css"; /** diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts index 2fc0469d..afed9a84 100644 --- a/packages/demo/src/main.ts +++ b/packages/demo/src/main.ts @@ -5,11 +5,11 @@ import "../../lib"; -// import "./fragment-development.css"; -// import App from "./AppFragmentDevelopment.svelte"; +import "./fragment-development.css"; +import App from "./AppFragmentDevelopment.svelte"; -import "./ccp.css"; -import App from "./AppCCP.svelte"; +// import "./ccp.css"; +// import App from "./AppCCP.svelte"; // import App from './AppBBMRI.svelte' // import './bbmri.css' diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts index efca31fa..2de0c842 100644 --- a/packages/lib/src/classes/spot.ts +++ b/packages/lib/src/classes/spot.ts @@ -4,7 +4,6 @@ import type { SiteData, Status, BeamResult } from "../types/response"; import type { ResponseStore } from "../types/backend"; -import { responseStore } from "../stores/response"; export class Spot { private currentTask!: string; @@ -53,6 +52,9 @@ export class Spot { console.info(`Created new Beam Task with id ${this.currentTask}`); + /** + * Listenes to the new_result event from beam and updates the response store + */ const eventSource = new EventSource( `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`, { @@ -61,7 +63,6 @@ export class Spot { ); eventSource.addEventListener("new_result", (message) => { const response: BeamResult = JSON.parse(message.data); - console.log("response", response); if (response.task !== this.currentTask) return; const site: string = response.from.split(".")[1]; const status: Status = response.status; @@ -70,11 +71,11 @@ export class Spot { ? JSON.parse(atob(response.body)) : null; - // updateResponse(changes); - responseStore.update((store: ResponseStore): ResponseStore => { - store.set(site, { status: status, data: body }); - return store; + const parsedResponse: ResponseStore = new Map().set(site, { + status: status, + data: body, }); + updateResponse(parsedResponse); }); // read error events from beam diff --git a/packages/lib/src/components/DataPasser.wc.svelte b/packages/lib/src/components/DataPasser.wc.svelte index 4a25b8c6..2b258709 100644 --- a/packages/lib/src/components/DataPasser.wc.svelte +++ b/packages/lib/src/components/DataPasser.wc.svelte @@ -19,6 +19,10 @@ import type { ResponseStore } from "../types/backend"; import type { AstTopLayer } from "../types/ast"; + /** + * Getters + */ + /** * returns the query store to the library user * @returns the query store @@ -28,25 +32,61 @@ }; /** - * lets the library user add a single stratifier to the query store - * @param label the value of the stratifier (e.g. "C31") - * @param catalogueGroupCode the code of the group where the stratifier is located (e.g. "diagnosis") - * @param queryGroupIndex the index of the query group where the stratifier should be added + * returns the response from the backend to the library user + * @returns the response from the backend */ + export const getResponseAPI = (): ResponseStore => { + return $responseStore; + }; - interface addStratifierToQueryAPIParams { - label: string; - catalogueGroupCode: string; - groupRange?: number; - queryGroupIndex?: number; - } + /** + * returns the AST to the library user + * @returns the AST + */ + export const getAstAPI = (): AstTopLayer => { + return buildAstFromQuery($queryStore); + }; + /** + * sets the response from the backend if any changes in status are detected + * @param response the response from the backend + */ + export const updateResponseStoreAPI = (response: ResponseStore): void => { + updateResponseStore(response); + }; + + /** + * returns the catalogue to the library user + * @param category the category name (e.g. "diagnosis") + * @returns array of strings containing the bottom level items' keys + */ + export const getCriteriaAPI = (category: string): string[] => { + return getCriteria(category); + }; + + /** + * Setters + */ + + /** + * lets the library user add a single stratifier to the query store + * @param params the parameters for the function + * @param params.label the value of the stratifier (e.g. "C31") + * @param params.catalogueGroupCode the code of the group where the stratifier is located (e.g. "diagnosis") + * @param params.groupRange of the numerical groups in charts + * @param params.queryGroupIndex the index of the query group where the stratifier should be added + */ export const addStratifierToQueryAPI = ({ label, catalogueGroupCode, groupRange, queryGroupIndex, - }: addStratifierToQueryAPIParams): void => { + }: { + label: string; + catalogueGroupCode: string; + groupRange?: number; + queryGroupIndex?: number; + }): void => { addStratifier({ label, catalogueGroupCode, @@ -58,76 +98,40 @@ /** * removes a query item from the query store - * @param queryObject the query object that should be removed - * @param queryGroupIndex the index of the query group where the stratifier should be removed + * @param params the parameters for the function + * @param params.queryObject the query object that should be removed + * @param params.queryGroupIndex the index of the query group where the stratifier should be removed */ - - interface RemoveItemFromQueryAPIParams { - queryObject: QueryItem; - queryGroupIndex?: number; - } - export const removeItemFromQuyeryAPI = ({ queryObject, queryGroupIndex = 0, - }: RemoveItemFromQueryAPIParams): void => { + }: { + queryObject: QueryItem; + queryGroupIndex?: number; + }): void => { removeItemFromQuery(queryObject, queryGroupIndex); }; /** * removes the value of a query item from the query store - * @param queryItem the query item from which the value should be removed from - * @param value the value that should be removed + * @param params the parameters for the function + * @param params.queryItem the query item from which the value should be removed from + * @param params.value the value that should be removed + * @param params.queryGroupIndex the index of the query group where the value should be removed */ - - interface RemoveValueFromQueryAPIParams { - queryItem: QueryItem; - value: QueryValue; - queryGroupIndex?: number; - } - export const removeValueFromQueryAPI = ({ queryItem, value, queryGroupIndex = 0, - }: RemoveValueFromQueryAPIParams): void => { + }: { + queryItem: QueryItem; + value: QueryValue; + queryGroupIndex?: number; + }): void => { const queryObject = { ...queryItem, values: [value], }; removeValueFromQuery(queryObject, queryGroupIndex); }; - - /** - * returns the response from the backend to the library user - * @returns the response from the backend - */ - export const getResponseAPI = (): ResponseStore => { - return $responseStore; - }; - - /** - * returns the AST to the library user - * @returns the AST - */ - export const getAstAPI = (): AstTopLayer => { - return buildAstFromQuery($queryStore); - }; - - /** - * sets the response from the backend if any changes in status are detected - * @param response the response from the backend - */ - export const updateResponseStoreAPI = (response: ResponseStore): void => { - updateResponseStore(response); - }; - - /** - * returns the catalogue to the library user - * @param category the category name (e.g. "diagnosis") - * @returns array of strings containing the bottom level items' keys - */ - export const getCriteriaAPI = (category: string): string[] => { - return getCriteria(category); - }; diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 08c89b70..eaa81931 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -40,7 +40,7 @@ $: options = $lensOptions?.backends as BackendOptions; - let controller: AbortController; + let controller: AbortController = new AbortController(); /** * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map diff --git a/packages/lib/src/stores/response.ts b/packages/lib/src/stores/response.ts index 99aa41bf..1fb13003 100644 --- a/packages/lib/src/stores/response.ts +++ b/packages/lib/src/stores/response.ts @@ -66,9 +66,9 @@ export const getSitePopulationForCode = ( code: string, ): number => { let population: number = 0; - if (!site) return population; + if (!site || !site.group) return population; - site.group.forEach((group) => { + site?.group?.forEach((group) => { if (group.code.text === code) { population += group.population[0].count; } @@ -134,7 +134,7 @@ export const getSitePopulationForStratumCode = ( stratumCode: string, stratifier: string, ): number => { - if (!site) return 0; + if (!site || !site.group) return 0; let population: number = 0; @@ -193,7 +193,7 @@ export const getSiteStratifierCodesForGroupCode = ( site: SiteData, code: string, ): string[] => { - if (!site) return [""]; + if (!site || !site.group) return [""]; const codes: string[] = []; site.group.forEach((groupItem) => { diff --git a/packages/lib/src/styles/searchbars.css b/packages/lib/src/styles/searchbars.css index 4d45a723..39b759f9 100644 --- a/packages/lib/src/styles/searchbars.css +++ b/packages/lib/src/styles/searchbars.css @@ -167,6 +167,51 @@ lens-search-bar-multiple::part(lens-searchbar-add-button) { margin-left: var(--gap-xs); } +/** +* Lens Search Bar Info Button +*/ + +lens-search-bar-multiple::part(info-button), +lens-search-bar::part(info-button){ + background-color: var(--blue); + border-color: var(--blue); + position: relative; + padding: 0; + border: 0; + top: +2px; +} + +lens-search-bar::part(info-button-icon), +lens-search-bar-multiple::part(info-button-icon) { + height: calc(var(--font-size-s) + 8px); + width: calc(var(--font-size-s) + 8px); + filter: brightness(0) invert(1); + box-sizing: content-box; + border-radius: 50%; +} + +lens-search-bar::part(info-button-icon):hover, +lens-search-bar-multiple::part(info-button-icon):hover { + cursor: pointer; +} + +lens-search-bar::part(info-button-dialogue), +lens-search-bar-multiple::part(info-button-dialogue) { + position: absolute; + border: none; + background-color: var(--white); + width: max-content; + max-width: 200px; + z-index: 100; + padding: var(--gap-s); + top: calc(var(--gap-m) + 4px); + right: -20px; + border: solid 1px var(--light-blue); + border-radius: var(--border-radius-small); + text-align: left; +} + + /** * delete buttons in searchbar and chips @@ -187,6 +232,7 @@ lens-search-bar-multiple::part(query-delete-button) { lens-search-bar::part(query-delete-button):hover, lens-search-bar-multiple::part(query-delete-button):hover { border: solid 1px var(--orange); + color: var(--orange) } lens-search-bar::part(query-delete-button-value), @@ -194,7 +240,7 @@ lens-search-bar-multiple::part(query-delete-button-value) { font-size: var(--font-size-xxs); color: var(--white); margin: 0 var(--gap-xs) 0 var(--gap-xxs); - background-color: var(--light-blue); + background-color: var(--blue); border: var(--white) 1px solid; } From 2172423d7386171c915cd6d8dda1cafefbe4dfa1 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 6 Jun 2024 09:08:05 +0200 Subject: [PATCH 07/22] feat(backend): add support for backend calls from project - with spot as example --- packages/demo/src/AppFragmentDevelopment.svelte | 10 +++++++++- packages/lib/src/components/Options.wc.svelte | 1 - .../buttons/SearchButtonComponenet.wc.svelte | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index f02dd924..b7fd7275 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -3,6 +3,7 @@ import type { CatalogueText, MeasureItem, + Measure, QueryEvent, QueryItem, QueryValue, @@ -50,6 +51,7 @@ ], }, ]; + console.log("dktkDiagnosisMeasure", dktkDiagnosisMeasure); /** * move to config file @@ -98,7 +100,7 @@ const event = e as QueryEvent; const { ast, updateResponse, abortController } = event.detail; - const measureItems = [ + const measureItems: MeasureItem[] = [ dktkPatientsMeasure, dktkDiagnosisMeasure, dktkSpecimenMeasure, @@ -107,8 +109,13 @@ dktkHistologyMeasure, ] as MeasureItem[]; + const measures: Measure[] = measureItems.map( + (measureItem: MeasureItem) => measureItem.measure, + ); + const criteria = dataPasser.getCriteriaAPI("diagnosis"); + console.log("app cql translation", measureItems); const cql = translateAstToCql( ast, false, @@ -116,6 +123,7 @@ measureItems, criteria, ); + console.log("app build meausre", measures); const library = buildLibrary(`${cql}`); const measure = buildMeasure(library.url, measures); diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index 9a6d2885..d0c44c59 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -17,7 +17,6 @@ import { catalogue } from "../stores/catalogue"; import { measureStore } from "../stores/measures"; import { iconStore } from "../stores/icons"; - import type { Criteria } from "../types/treeData"; import type { MeasureStore } from "../types/backend"; import type { Criteria } from "../types/treeData"; import type { LensOptions } from "../types/options"; diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index eaa81931..0f4e2575 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -57,12 +57,12 @@ }); $: catalogueKeyToResponseKeyMap.update((mappings) => { - options?.spots.forEach((spot) => { + options?.spots?.forEach((spot) => { spot.catalogueKeyToResponseKeyMap.forEach((mapping) => { mappings.set(mapping[0], mapping[1]); }); }); - options?.blazes.forEach((blaze: BlazeOption) => { + options?.blazes?.forEach((blaze: BlazeOption) => { blaze.catalogueKeyToResponseKeyMap.forEach((mapping) => { mappings.set(mapping[0], mapping[1]); }); @@ -86,7 +86,7 @@ const ast = buildAstFromQuery($queryStore); - options.spots.forEach((spot: SpotOption) => { + options?.spots?.forEach((spot: SpotOption) => { const name = spot.name; const measureItem: MeasureOption | undefined = $measureStore.find( (measureStoreItem: MeasureOption) => @@ -122,7 +122,7 @@ ); }); - options.blazes.forEach((blaze: BlazeOption) => { + options?.blazes?.forEach((blaze: BlazeOption) => { const { name, url, From 547df288a8de7217c73d4f99bb72bd32c4197378 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 6 Jun 2024 09:22:37 +0200 Subject: [PATCH 08/22] feat(backend): add config for backend urls where ast is sent --- .../buttons/SearchButtonComponenet.wc.svelte | 20 +++++++++++++++++++ packages/lib/src/types/backend.ts | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 0f4e2575..88d21daf 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -156,11 +156,31 @@ backend.send(cql, controller, measures); }); + options?.customAstBackends?.forEach((customAstBackendUrl: string) => { + customBackendCallWithAst(ast, customAstBackendUrl); + }); emitEvent(ast); queryModified.set(false); }; + const customBackendCallWithAst = (ast: AstTopLayer, url: string): void => { + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ast), + }) + .then((response) => response.json()) + .then((data) => { + updateResponseStore(data); + }) + .catch((error) => { + console.error("Error:", error); + }); + }; + interface QueryEvent extends Event { detail: { ast: AstTopLayer; diff --git a/packages/lib/src/types/backend.ts b/packages/lib/src/types/backend.ts index d3817baa..cc439d67 100644 --- a/packages/lib/src/types/backend.ts +++ b/packages/lib/src/types/backend.ts @@ -75,8 +75,9 @@ export type BlazeOption = { }; export type BackendOptions = { - spots: SpotOption[]; - blazes: BlazeOption[]; + spots?: SpotOption[]; + blazes?: BlazeOption[]; + customAstBackends?: string[]; }; export interface QueryEvent extends Event { From 067312a37e95a271f80c56446b7ea6fbdc87a0b2 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 6 Jun 2024 09:24:41 +0200 Subject: [PATCH 09/22] feat(documentation): add documentation --- .../src/components/buttons/SearchButtonComponenet.wc.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index 88d21daf..c8b85d83 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -164,6 +164,11 @@ queryModified.set(false); }; + /** + * Sends the ast to a custom backend + * @param ast the ast to be sent to the backend + * @param url the url of the backend + */ const customBackendCallWithAst = (ast: AstTopLayer, url: string): void => { fetch(url, { method: "POST", From 8e9789f63f309467548256497b97ce376c262da6 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 6 Jun 2024 10:33:03 +0200 Subject: [PATCH 10/22] feat(config): add backend to ui site mapping option --- packages/demo/public/options.json | 21 +++++++++++++++++-- packages/lib/src/components/Options.wc.svelte | 21 +++++++++++++++++++ .../buttons/SearchButtonComponenet.wc.svelte | 19 +---------------- .../results/ResultTableComponent.wc.svelte | 5 +---- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index 211f37b8..e54f859a 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options.json @@ -5,6 +5,24 @@ "text": "Add all" } }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, "chartOptions": { "patients": { "legendMapping": { @@ -171,8 +189,7 @@ { "stratifierCode": "Histologies", "stratumCode": "1" - }, - {} + } ] } ], diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index d0c44c59..1a693870 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -20,11 +20,16 @@ import type { MeasureStore } from "../types/backend"; import type { Criteria } from "../types/treeData"; import type { LensOptions } from "../types/options"; + import { uiSiteMappingsStore } from "../stores/mappings"; export let options: LensOptions = {}; export let catalogueData: Criteria[] = []; export let measures: MeasureStore = {} as MeasureStore; + /** + * updates the icon store with the options passed in + * @param options the Lens options + */ const updateIconStore = (options: LensOptions): void => { iconStore.update((store) => { if (typeof options === "object" && "iconOptions" in options) { @@ -61,6 +66,22 @@ }); }; + /** + * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map + * web components' props are json, meaning that Maps are not supported + * therefore it's a 2d array of strings which is converted to a map + */ + $: uiSiteMappingsStore.update((mappings) => { + if (!options?.siteMappings) return mappings; + console.log("options", options); + console.log(Object.entries(options?.siteMappings)); + Object.entries(options?.siteMappings)?.forEach((site) => { + mappings.set(site[0], site[1]); + }); + + return mappings; + }); + $: $lensOptions = options; $: updateIconStore(options); $: $catalogue = catalogueData; diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte index c8b85d83..c84ddf1a 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -17,10 +17,7 @@ import { buildLibrary, buildMeasure } from "../../helpers/cql-measure"; import { Spot } from "../../classes/spot"; import { Blaze } from "../../classes/blaze"; - import { - catalogueKeyToResponseKeyMap, - uiSiteMappingsStore, - } from "../../stores/mappings"; + import { catalogueKeyToResponseKeyMap } from "../../stores/mappings"; import { responseStore, updateResponseStore } from "../../stores/response"; import { lensOptions } from "../../stores/options"; import type { @@ -42,20 +39,6 @@ let controller: AbortController = new AbortController(); - /** - * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map - * web components' props are json, meaning that Maps are not supported - * therefore it's a 2d array of strings which is converted to a map - */ - $: uiSiteMappingsStore.update((mappings) => { - options?.spots.forEach((spot) => { - spot.uiSiteMap.forEach((site) => { - mappings.set(site[0], site[1]); - }); - }); - return mappings; - }); - $: catalogueKeyToResponseKeyMap.update((mappings) => { options?.spots?.forEach((spot) => { spot.catalogueKeyToResponseKeyMap.forEach((mapping) => { diff --git a/packages/lib/src/components/results/ResultTableComponent.wc.svelte b/packages/lib/src/components/results/ResultTableComponent.wc.svelte index e51f5f24..c32a4547 100644 --- a/packages/lib/src/components/results/ResultTableComponent.wc.svelte +++ b/packages/lib/src/components/results/ResultTableComponent.wc.svelte @@ -24,10 +24,7 @@ let claimedText: string; $: claimedText = - (($lensOptions?.tableOptions && - $lensOptions.tableOptions?.claimedText && - $lensOptions.tableOptions.claimedText) as string) || - "Processing..."; + ($lensOptions?.tableOptions?.claimedText as string) || "Processing..."; /** * data-types for the table From 03d9af01f4d50daf7624056322b868cfd70373f1 Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Thu, 6 Jun 2024 11:00:47 +0200 Subject: [PATCH 11/22] feat(options): add site mapping update schema --- packages/demo/public/options.json | 75 +----------- .../demo/src/AppFragmentDevelopment.svelte | 1 - packages/lib/src/components/Options.wc.svelte | 2 - packages/lib/src/types/options.schema.json | 110 ++++++++++++++++++ 4 files changed, 113 insertions(+), 75 deletions(-) diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index c5d73e45..e781695f 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options.json @@ -211,6 +211,9 @@ ] }, "backends": { + "customBackends": [ + "someUrl" + ], "spots": [ { "name": "DKTK", @@ -232,72 +235,6 @@ "dktk-test", "hamburg" ], - "uiSiteMap": [ - [ - "berlin", - "Berlin" - ], - [ - "berlin-test", - "Berlin Test" - ], - [ - "bonn", - "Bonn" - ], - [ - "dresden", - "Dresden" - ], - [ - "essen", - "Essen" - ], - [ - "frankfurt", - "Frankfurt" - ], - [ - "freiburg", - "Freiburg" - ], - [ - "hannover", - "Hannover" - ], - [ - "mainz", - "Mainz" - ], - [ - "muenchen-lmu", - "München(LMU)" - ], - [ - "muenchen-tum", - "München(TUM)" - ], - [ - "ulm", - "Ulm" - ], - [ - "wuerzburg", - "Würzburg" - ], - [ - "mannheim", - "Mannheim" - ], - [ - "dktk-test", - "DKTK-Test" - ], - [ - "hamburg", - "Hamburg" - ] - ], "catalogueKeyToResponseKeyMap": [ [ "gender", @@ -335,12 +272,6 @@ "name": "DKTK", "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", "url": "http://localhost:8080", - "uiSiteMap": [ - [ - "blaze1", - "Blaze 1" - ] - ], "catalogueKeyToResponseKeyMap": [ [ "gender", diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index b7fd7275..cd0501f9 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -51,7 +51,6 @@ ], }, ]; - console.log("dktkDiagnosisMeasure", dktkDiagnosisMeasure); /** * move to config file diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index f6dc0b21..600ac086 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -116,8 +116,6 @@ */ $: uiSiteMappingsStore.update((mappings) => { if (!options?.siteMappings) return mappings; - console.log("options", options); - console.log(Object.entries(options?.siteMappings)); Object.entries(options?.siteMappings)?.forEach((site) => { mappings.set(site[0], site[1]); }); diff --git a/packages/lib/src/types/options.schema.json b/packages/lib/src/types/options.schema.json index d455c696..42bdfc2f 100644 --- a/packages/lib/src/types/options.schema.json +++ b/packages/lib/src/types/options.schema.json @@ -35,6 +35,18 @@ "unevaluatedProperties": false, "required": [] }, + "siteMappings":{ + "type": "object", + "patternProperties": { + "^.+$": { + "type": "string", + "pattern": "^.+$" + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] + }, "chartOptions": { "type": "object", "patternProperties": { @@ -235,6 +247,104 @@ "additionalProperties": false, "unevaluatedProperties": false, "required": [] + }, + "backends": { + "type": "object", + "properties": { + "customBackends": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the custom backend" + } + }, + "spots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^.+$", + "description": "The name of the spot" + }, + "backendMeasures": { + "type": "string", + "pattern": "^.+$", + "description": "The measures of the spot" + }, + "url": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the spot" + }, + "sites": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The sites of the spot" + } + }, + "catalogueKeyToResponseKeyMap": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The mapping of the catalogue key to the response key" + } + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": ["name", "backendMeasures", "url", "sites"] + } + }, + "blazes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^.+$", + "description": "The name of the blaze" + }, + "backendMeasures": { + "type": "string", + "pattern": "^.+$", + "description": "The measures of the blaze" + }, + "url": { + "type": "string", + "pattern": "^.+$", + "description": "The URL of the blaze" + }, + "catalogueKeyToResponseKeyMap": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+$", + "description": "The mapping of the catalogue key to the response key" + } + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": ["name", "backendMeasures", "url"] + } + } + }, + "additionalProperties": false, + "unevaluatedProperties": false, + "required": [] } }, "additionalProperties": false, From 15f30ca906965bdcbf48010cd1925dbecb983e0b Mon Sep 17 00:00:00 2001 From: Mats Johansen Date: Wed, 12 Jun 2024 10:05:07 +0200 Subject: [PATCH 12/22] refactor(interfaces): refactor interfaces add different options for different environments --- packages/demo/public/options-ccp-demo.json | 305 ++++++++++++++++++ packages/demo/public/options-ccp-prod.json | 268 +++++++++++++++ .../public/{options.json => options-dev.json} | 0 packages/demo/src/AppCCP.svelte | 96 ++---- .../demo/src/AppFragmentDevelopment.svelte | 6 +- .../lib/src/components/DataPasser.wc.svelte | 26 +- packages/lib/src/types/DataPasser.d.ts | 10 - packages/lib/src/types/backend.ts | 7 +- packages/lib/src/types/dataPasser.ts | 32 ++ 9 files changed, 644 insertions(+), 106 deletions(-) create mode 100644 packages/demo/public/options-ccp-demo.json create mode 100644 packages/demo/public/options-ccp-prod.json rename packages/demo/public/{options.json => options-dev.json} (100%) delete mode 100644 packages/lib/src/types/DataPasser.d.ts create mode 100644 packages/lib/src/types/dataPasser.ts diff --git a/packages/demo/public/options-ccp-demo.json b/packages/demo/public/options-ccp-demo.json new file mode 100644 index 00000000..882fcb1c --- /dev/null +++ b/packages/demo/public/options-ccp-demo.json @@ -0,0 +1,305 @@ +{ + "iconOptions": { + "deleteUrl": "delete_icon.svg", + "infoUrl": "info-circle-svgrepo-com.svg", + "selectAll": { + "text": "Add all" + } + }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, + "chartOptions": { + "patients": { + "legendMapping": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + } + }, + "gender": { + "legendMapping": { + "male": "Männlich", + "female": "Weiblich", + "unknown": "Unbekannt", + "other": "Divers" + } + }, + "diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen." + ] + }, + "age_at_diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. " + ] + }, + "75186-7": { + "hintText": [ + "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.", + "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.", + "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt." + ] + }, + "therapy_of_tumor": { + "aggregations": [ + "medicationStatements" + ], + "tooltips": { + "OP": "Operationen", + "ST": "Strahlentherapien", + "medicationStatements": "Systemische Therapien" + } + }, + "medicationStatements": { + "hintText": [ + "Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021)" + ], + "tooltips": { + "CH": "Chemotherapie", + "HO": "Hormontherapie", + "IM": "Immun-/Antikörpertherapie", + "KM": "Knochenmarktransplantation", + "ZS": "zielgerichtete Substanzen", + "CI": "Chemo- + Immun-/Antikörpertherapie", + "CZ": "Chemotherapie + zielgerichtete Substanzen", + "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)", + "AS": "Active Surveillance", + "WS": "Wait and see", + "WW": "Watchful Waiting", + "SO": "Sonstiges" + } + }, + "sample_kind": { + "hintText": [ + "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind." + ], + "accumulatedValues": [ + { + "name": "ffpe-tissue", + "values": [ + "tissue-ffpe", + "tumor-tissue-ffpe", + "normal-tissue-ffpe", + "other-tissue-ffpe" + ] + }, + { + "name": "frozen-tissue", + "values": [ + "tissue-frozen", + "tumor-tissue-frozen", + "normal-tissue-frozen", + "other-tissue-frozen" + ] + } + ], + "tooltips": { + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + }, + "legendMapping":{ + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + } + } + }, + "tableOptions": { + "headerData": [ + { + "title": "Standorte", + "dataKey": "site" + }, + { + "title": "Patienten", + "dataKey": "patients" + }, + { + "title": "Bioproben*", + "aggregatedDataKeys": [ + { + "groupCode": "specimen" + }, + { + "stratifierCode": "Histologies", + "stratumCode": "1" + } + ] + } + ], + "claimedText": "Processing..." + }, + "resultSummaryOptions": { + "title": "Ergebnisse", + "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.", + "dataTypes": [ + { + "title": "Standorte", + "dataKey": "collections" + }, + { + "title": "Patienten", + "dataKey": "patients" + } + ] + }, + "backends": { + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "https://backend.demo.lens.samply.de/prod/", + "sites": [ + "berlin", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ], + "blazes": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "http://localhost:8080", + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ] + } +} diff --git a/packages/demo/public/options-ccp-prod.json b/packages/demo/public/options-ccp-prod.json new file mode 100644 index 00000000..d783ae1b --- /dev/null +++ b/packages/demo/public/options-ccp-prod.json @@ -0,0 +1,268 @@ +{ + "iconOptions": { + "deleteUrl": "delete_icon.svg", + "infoUrl": "info-circle-svgrepo-com.svg", + "selectAll": { + "text": "Add all" + } + }, + "siteMappings": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + }, + "chartOptions": { + "patients": { + "legendMapping": { + "berlin": "Berlin", + "berlin-test": "Berlin", + "bonn": "Bonn", + "dresden": "Dresden", + "essen": "Essen", + "frankfurt": "Frankfurt", + "freiburg": "Freiburg", + "hannover": "Hannover", + "mainz": "Mainz", + "muenchen-lmu": "München(LMU)", + "muenchen-tum": "München(TUM)", + "ulm": "Ulm", + "wuerzburg": "Würzburg", + "mannheim": "Mannheim", + "dktk-test": "DKTK-Test", + "hamburg": "Hamburg" + } + }, + "gender": { + "legendMapping": { + "male": "Männlich", + "female": "Weiblich", + "unknown": "Unbekannt", + "other": "Divers" + } + }, + "diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen." + ] + }, + "age_at_diagnosis": { + "hintText": [ + "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. " + ] + }, + "75186-7": { + "hintText": [ + "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.", + "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.", + "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt." + ] + }, + "therapy_of_tumor": { + "aggregations": [ + "medicationStatements" + ], + "tooltips": { + "OP": "Operationen", + "ST": "Strahlentherapien", + "medicationStatements": "Systemische Therapien" + } + }, + "medicationStatements": { + "hintText": [ + "Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021)" + ], + "tooltips": { + "CH": "Chemotherapie", + "HO": "Hormontherapie", + "IM": "Immun-/Antikörpertherapie", + "KM": "Knochenmarktransplantation", + "ZS": "zielgerichtete Substanzen", + "CI": "Chemo- + Immun-/Antikörpertherapie", + "CZ": "Chemotherapie + zielgerichtete Substanzen", + "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen", + "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)", + "AS": "Active Surveillance", + "WS": "Wait and see", + "WW": "Watchful Waiting", + "SO": "Sonstiges" + } + }, + "sample_kind": { + "hintText": [ + "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind." + ], + "accumulatedValues": [ + { + "name": "ffpe-tissue", + "values": [ + "tissue-ffpe", + "tumor-tissue-ffpe", + "normal-tissue-ffpe", + "other-tissue-ffpe" + ] + }, + { + "name": "frozen-tissue", + "values": [ + "tissue-frozen", + "tumor-tissue-frozen", + "normal-tissue-frozen", + "other-tissue-frozen" + ] + } + ], + "tooltips": { + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + }, + "legendMapping":{ + "ffpe-tissue": "Gewebe FFPE", + "frozen-tissue": "Gewebe schockgefroren", + "tissue-other": "Gewebe, Andere Konservierungsart", + "whole-blood": "Vollblut", + "blood-serum": "Serum", + "blood-plasma": "Plasma", + "buffy-coat": "Buffy Coat", + "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)", + "dried-whole-blood": "Blutkarten", + "swab": "Abstrich", + "ascites": "Aszites", + "stool-faeces": "Stuhl", + "urine": "Urin", + "csf-liquor": "Liquor", + "bone-marrow": "Knochenmark", + "saliva": "Speichel", + "liquid-other": "Flüssigprobe, Andere", + "dna": "DNA", + "rna": "RNA", + "derivative-other": "Derivat, Andere" + } + } + }, + "tableOptions": { + "headerData": [ + { + "title": "Standorte", + "dataKey": "site" + }, + { + "title": "Patienten", + "dataKey": "patients" + }, + { + "title": "Bioproben*", + "aggregatedDataKeys": [ + { + "groupCode": "specimen" + }, + { + "stratifierCode": "Histologies", + "stratumCode": "1" + } + ] + } + ], + "claimedText": "Processing..." + }, + "resultSummaryOptions": { + "title": "Ergebnisse", + "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.", + "dataTypes": [ + { + "title": "Standorte", + "dataKey": "collections" + }, + { + "title": "Patienten", + "dataKey": "patients" + } + ] + }, + "backends": { + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "https://backend.data.dktk.dkfz.de/prod/", + "sites": [ + "berlin", + "dresden", + "essen", + "frankfurt", + "freiburg", + "hannover", + "mainz", + "muenchen-lmu", + "muenchen-tum", + "ulm", + "wuerzburg", + "mannheim", + "dktk-test", + "hamburg" + ], + "catalogueKeyToResponseKeyMap": [ + [ + "gender", + "Gender" + ], + [ + "age_at_diagnosis", + "Age" + ], + [ + "diagnosis", + "diagnosis" + ], + [ + "medicationStatements", + "MedicationType" + ], + [ + "sample_kind", + "sample_kind" + ], + [ + "therapy_of_tumor", + "ProcedureType" + ], + [ + "75186-7", + "75186-7" + ] + ] + } + ] + } +} diff --git a/packages/demo/public/options.json b/packages/demo/public/options-dev.json similarity index 100% rename from packages/demo/public/options.json rename to packages/demo/public/options-dev.json diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index a43aa57f..5cd13fe6 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -1,6 +1,9 @@
diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index cd0501f9..8cc6be2c 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -1,5 +1,4 @@