diff --git a/packages/demo/public/options.json b/packages/demo/public/options.json index 7dc8d0f..e9b379e 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 616df34..41e122d 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 0000000..a475acd --- /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 0000000..f472510 --- /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 f472510..62b32d7 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 5d9676a..c9e234c 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 a3db079..d21185e 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 46d2866..6305c6d 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 62974bc..31c0769 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[]; +};