diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index db1ea38c..937c4ffb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -21,11 +21,13 @@ services: spot: image: samply/rustyspot:main ports: - - 8080:8080 + - 8055:8055 environment: + RUST_LOG: "info" + CORS_ORIGIN: http://localhost:5173 BEAM_SECRET: "${LOCAL_BEAM_SECRET_SPOT}" BEAM_PROXY_URL: http://beam-proxy:8081 - BEAM_APP_ID: "spot.${LOCAL_BEAM_ID}.${BROKER_HOST}" + BEAM_APP_ID: "focus.${LOCAL_BEAM_ID}.${BROKER_HOST}" depends_on: - "beam-proxy" profiles: diff --git a/docker-compose.yml b/docker-compose.yml index 1c06385d..7bf498cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,8 @@ services: spot: image: samply/rustyspot:main + ports: + - "8055:8055" environment: HTTP_PROXY: ${http_proxy} HTTPS_PROXY: ${https_proxy} diff --git a/example.env b/example.env index 66837680..2e095cf5 100644 --- a/example.env +++ b/example.env @@ -11,6 +11,7 @@ GUI_HOST="data.dktk.dkfz.de|demo.lens.samply.de" BROKER_HOST="broker.ccp-it.dktk.dkfz.de" LOCAL_BEAM_ID="your-proxy-id" LOCAL_BEAM_SECRET_SPOT="insert-a-random-passphrase-here" +LOCAL_BEAM_SECRET_PRISM="insert-a-random-passphrase-here" # Request your OAUTH client from your oauth provider admin OAUTH_ISSUER_URL="the-discovery-adress-of-your-oauth-provider" diff --git a/options_tester.cjs b/options_tester.cjs index 9fdfc808..9c41892e 100644 --- a/options_tester.cjs +++ b/options_tester.cjs @@ -1,17 +1,32 @@ "use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { +const __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; + +console.log( + "Checking Lens options for ", + process.env.VITE_TARGET_ENVIRONMENT, +); + +let optionsPath = ""; +if (process.env.VITE_TARGET_ENVIRONMENT === "production") { + optionsPath = "./packages/demo/public/options-ccp-prod.json"; +} else if (process.env.VITE_TARGET_ENVIRONMENT === "staging") { + optionsPath = "./packages/demo/public/options-ccp-demo.json"; +} else { + optionsPath = "./packages/demo/public/options-dev.json"; +} + Object.defineProperty(exports, "__esModule", { value: true }); -var options_schema_json_1 = __importDefault(require("./packages/lib/src/interfaces/options.schema.json")); -var schemasafe_1 = require("@exodus/schemasafe"); -var options_json_1 = __importDefault(require("./packages/demo/public/options.json")); +const options_schema_json_1 = __importDefault(require("./packages/lib/src/types/options.schema.json")); +const schemasafe_1 = require("@exodus/schemasafe"); +const options_json_1 = __importDefault(require(optionsPath)); console.log("Checking Lens options"); -var parse = (0, schemasafe_1.parser)(options_schema_json_1.default, { +const parse = (0, schemasafe_1.parser)(options_schema_json_1.default, { includeErrors: true, allErrors: true, }); -var validJSON = parse(JSON.stringify(options_json_1.default)); +const validJSON = parse(JSON.stringify(options_json_1.default)); if (validJSON.valid === true) { console.log("Options are valid"); } diff --git a/options_tester.ts b/options_tester.ts index 296c6d26..d7751695 100644 --- a/options_tester.ts +++ b/options_tester.ts @@ -1,9 +1,23 @@ -import optionsSchema from "./packages/lib/src/interfaces/options.schema.json"; +import optionsSchema from "./packages/lib/src/types/options.schema.json"; import { parser } from "@exodus/schemasafe"; -import options from "./packages/demo/public/options.json"; +import devOptions from "./packages/demo/public/options-dev.json"; +import demoOptions from "./packages/demo/public/options-ccp-demo.json"; +import prodOptions from "./packages/demo/public/options-ccp-prod.json"; -console.log("Checking Lens options"); +console.log( + "Checking Lens options for ", + import.meta.env.VITE_TARGET_ENVIRONMENT, +); + +let options = {}; +if (import.meta.env.VITE_TARGET_ENVIRONMENT === "production") { + options = prodOptions; +} else if (import.meta.env.VITE_TARGET_ENVIRONMENT === "staging") { + options = demoOptions; +} else { + options = devOptions; +} const parse = parser(optionsSchema, { includeErrors: true, diff --git a/package-lock.json b/package-lock.json index 165a14b0..a424b458 100644 --- a/package-lock.json +++ b/package-lock.json @@ -720,7 +720,8 @@ "node_modules/@exodus/schemasafe": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==" + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" }, "node_modules/@hapi/hoek": { "version": "9.3.0", @@ -2985,9 +2986,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -2995,6 +2996,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5645,10 +5647,11 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", diff --git a/packages/demo/public/options-ccp-demo.json b/packages/demo/public/options-ccp-demo.json new file mode 100644 index 00000000..cc44bc01 --- /dev/null +++ b/packages/demo/public/options-ccp-demo.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.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" + ] + ] + } + ] + } +} diff --git a/packages/demo/public/options.json b/packages/demo/public/options-ccp-prod.json similarity index 77% rename from packages/demo/public/options.json rename to packages/demo/public/options-ccp-prod.json index 466d9019..4888e2c4 100644 --- a/packages/demo/public/options.json +++ b/packages/demo/public/options-ccp-prod.json @@ -6,6 +6,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": { @@ -194,5 +212,60 @@ "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-dev.json b/packages/demo/public/options-dev.json new file mode 100644 index 00000000..e781695f --- /dev/null +++ b/packages/demo/public/options-dev.json @@ -0,0 +1,308 @@ +{ + "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": { + "customBackends": [ + "someUrl" + ], + "spots": [ + { + "name": "DKTK", + "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION", + "url": "http://localhost:8055", + "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/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte index e24d0416..07e48b41 100644 --- a/packages/demo/src/AppCCP.svelte +++ b/packages/demo/src/AppCCP.svelte @@ -1,4 +1,9 @@
@@ -147,12 +112,30 @@ noQueryMessage="Leere Suchanfrage: Sucht nach allen Ergebnissen." showQuery={true} /> - + + +
+
+

Suchkriterien

+ + + +
@@ -310,4 +293,5 @@ > - + + diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte index 9c7e7a80..f14019a5 100644 --- a/packages/demo/src/AppFragmentDevelopment.svelte +++ b/packages/demo/src/AppFragmentDevelopment.svelte @@ -1,5 +1,13 @@
@@ -193,11 +193,7 @@

Search Button

- +

Result Summary Bar

@@ -207,7 +203,7 @@

Result Table

- +

Result Pie Chart

@@ -242,4 +238,5 @@
- + + diff --git a/packages/demo/src/backends/ast-to-cql-translator.ts b/packages/demo/src/backends/ast-to-cql-translator.ts new file mode 100644 index 00000000..6b6a8148 --- /dev/null +++ b/packages/demo/src/backends/ast-to-cql-translator.ts @@ -0,0 +1,380 @@ +/** + * TODO: Document this file. Move to Project + */ + +import type { + AstBottomLayerValue, + AstElement, + AstTopLayer, + MeasureItem, +} from "../../../../dist/types"; +import { + alias as aliasMap, + cqltemplate, + criterionMap, +} from "./cqlquery-mappings"; + +let codesystems: string[] = []; +let criteria: string[]; + +export const translateAstToCql = ( + query: AstTopLayer, + returnOnlySingeltons: boolean = true, + backendMeasures: string, + measures: MeasureItem[], + criterionList: string[], +): string => { + criteria = criterionList; + + /** + * DISCUSS: why is this even an array? + * in bbmri there is only concatted to the string + */ + codesystems = [ + // NOTE: We always need loinc, as the Deceased Stratifier is computed with it!!! + "codesystem loinc: 'http://loinc.org'", + ]; + + const cqlHeader = + "library Retrieve\n" + + "using FHIR version '4.0.0'\n" + + "include FHIRHelpers version '4.0.0'\n" + + "\n"; + + let singletons: string = ""; + singletons = backendMeasures; + singletons += resolveOperation(query); + + if (query.children.length == 0) { + singletons += "\ntrue"; + } + + if (returnOnlySingeltons) { + return singletons; + } + + return ( + cqlHeader + + getCodesystems() + + "context Patient\n" + + measures.map((measureItem: MeasureItem) => measureItem.cql).join("") + + singletons + ); +}; + +const resolveOperation = (operation: AstElement): string => { + let expression: string = ""; + + if ("children" in operation && operation.children.length > 1) { + expression += "("; + } + + "children" in operation && + operation.children.forEach((element: AstElement, index) => { + if ("children" in element) { + expression += resolveOperation(element); + } + if ( + "key" in element && + "type" in element && + "system" in element && + "value" in element + ) { + expression += getSingleton(element); + } + if (index < operation.children.length - 1) { + expression += + ")" + ` ${operation.operand.toLowerCase()} ` + "\n("; + } else { + if (operation.children.length > 1) { + expression += ")"; + } + } + }); + + return expression; +}; + +const getSingleton = (criterion: AstBottomLayerValue): string => { + let expression: string = ""; + + //TODO: Workaround for using the value of "Therapy of Tumor" as key. Need an additional field in catalogue + if (criterion.key === "therapy_of_tumor") { + criterion.key = criterion.value as string; + } + + const myCriterion = criterionMap.get(criterion.key); + + if (myCriterion) { + const myCQL = cqltemplate.get(myCriterion.type); + 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": + case "uiccstadium": + case "observationMetastasis": + case "observationMetastasisBodySite": + case "procedure": + case "procedureResidualstatus": + case "medicationStatement": + case "specimen": + case "BBMRI_specimen": + case "BBMRI_hasSpecimen": + case "hasSpecimen": + case "Organization": + case "observationMolecularMarkerName": + case "observationMolecularMarkerAminoacidchange": + case "observationMolecularMarkerDNAchange": + case "observationMolecularMarkerSeqRefNCBI": + case "observationMolecularMarkerEnsemblID": + case "department": + case "TNMp": + 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) === "%" && + criterion.value.length == 5 + ) { + const mykey = criterion.value.slice(0, -2); + if (criteria != undefined) { + const expandedValues = criteria.filter( + (value) => value.startsWith(mykey), + ); + expression += getSingleton({ + key: criterion.key, + type: criterion.type, + system: criterion.system, + value: expandedValues, + }); + } + } else if ( + criterion.value.slice(-1) === "%" && + criterion.value.length == 6 + ) { + const mykey = criterion.value.slice(0, -1); + if (criteria != undefined) { + const expandedValues = criteria.filter( + (value) => value.startsWith(mykey), + ); + expandedValues.push( + criterion.value.slice(0, 5), + ); + expression += getSingleton({ + key: criterion.key, + type: criterion.type, + system: criterion.system, + value: expandedValues, + }); + } + } else { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value as string, + ); + } + } + if (typeof criterion.value === "boolean") { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + ); + } + + if (criterion.value instanceof Array) { + if (criterion.value.length === 1) { + expression += substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + criterion.value[0], + ); + } else { + criterion.value.forEach((value: string) => { + expression += + "(" + + substituteCQLExpression( + criterion.key, + myCriterion.alias, + myCQL, + value, + ) + + ") or\n"; + }); + expression = expression.slice(0, -4); + } + } + + break; + } + + case "conditionRangeDate": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "condition", + "Date", + myCQL, + ); + break; + } + + case "primaryConditionRangeDate": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "primaryCondition", + "Date", + myCQL, + ); + break; + } + + case "conditionRangeAge": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "condition", + "Age", + myCQL, + ); + break; + } + + case "primaryConditionRangeAge": { + expression += substituteRangeCQLExpression( + criterion, + myCriterion, + "primaryCondition", + "Age", + myCQL, + ); + break; + } + } + } + } + return expression; +}; + +const substituteRangeCQLExpression = ( + criterion: AstBottomLayerValue, + myCriterion: { type: string; alias?: string[] }, + criterionPrefix: string, + criterionSuffix: string, + rangeCQL: string, +): string => { + const input = criterion.value as { min: number; max: number }; + if (input === null) { + console.warn( + `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as it is not of type {min: number, max: number}!`, + ); + return ""; + } + if (input.min === 0 && input.max === 0) { + console.warn( + `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as both dates are undefined!`, + ); + return ""; + } else if (input.min === 0) { + const lowerThanDateTemplate = cqltemplate.get( + `${criterionPrefix}LowerThan${criterionSuffix}`, + ); + if (lowerThanDateTemplate) + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + lowerThanDateTemplate, + "", + input.min, + input.max, + ); + } else if (input.max === 0) { + const greaterThanDateTemplate = cqltemplate.get( + `${criterionPrefix}GreaterThan${criterionSuffix}`, + ); + if (greaterThanDateTemplate) + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + greaterThanDateTemplate, + "", + input.min, + input.max, + ); + } else { + return substituteCQLExpression( + criterion.key, + myCriterion.alias, + rangeCQL, + "", + input.min, + input.max, + ); + } + return ""; +}; + +const substituteCQLExpression = ( + key: string, + alias: string[] | undefined, + cql: string, + value?: string, + min?: number, + max?: number, +): string => { + let cqlString: string; + if (value) { + cqlString = cql.replace(/{{C}}/g, value); + } else { + cqlString = cql; + } + cqlString = cqlString.replace(new RegExp("{{K}}"), key); + if (alias && alias[0]) { + cqlString = cqlString.replace(new RegExp("{{A1}}", "g"), alias[0]); + const systemExpression = + "codesystem " + alias[0] + ": '" + aliasMap.get(alias[0]) + "'"; + if (!codesystems.includes(systemExpression)) { + codesystems.push(systemExpression); + } + } + if (alias && alias[1]) { + cqlString = cqlString.replace(new RegExp("{{A2}}", "g"), alias[1]); + const systemExpression = + "codesystem " + alias[1] + ": '" + aliasMap.get(alias[1]) + "'"; + if (!codesystems.includes(systemExpression)) { + codesystems.push(systemExpression); + } + } + if (min || min === 0) { + cqlString = cqlString.replace(new RegExp("{{D1}}"), min.toString()); + } + if (max || max === 0) { + cqlString = cqlString.replace(new RegExp("{{D2}}"), max.toString()); + } + return cqlString; +}; + +const getCodesystems = (): string => { + let codesystemString: string = ""; + + codesystems.forEach((systems) => { + codesystemString += systems + "\n"; + }); + + if (codesystems.length > 0) { + codesystemString += "\n"; + } + + return codesystemString; +}; 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/cql-measure.ts b/packages/demo/src/backends/cql-measure.ts new file mode 100644 index 00000000..21f3b59a --- /dev/null +++ b/packages/demo/src/backends/cql-measure.ts @@ -0,0 +1,92 @@ +import { v4 as uuidv4 } from "uuid"; +import type { Measure } from "../types/backend"; + +type BuildLibraryReturn = { + resourceType: string; + url: string; + status: string; + type: { + coding: { + system: string; + code: string; + }[]; + }; + content: { + contentType: string; + data: string; + }[]; +}; + +export const buildLibrary = (cql: string): BuildLibraryReturn => { + const libraryId = uuidv4(); + const encodedQuery = btoa(unescape(encodeURIComponent(cql))); + return { + resourceType: "Library", + url: "urn:uuid:" + libraryId, + status: "active", + type: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/library-type", + code: "logic-library", + }, + ], + }, + content: [ + { + contentType: "text/cql", + data: encodedQuery, + }, + ], + }; +}; + +type BuildMeasureReturn = { + resourceType: string; + url: string; + status: string; + subjectCodeableConcept: { + coding: { + system: string; + code: string; + }[]; + }; + library: string; + scoring: { + coding: { + system: string; + code: string; + }[]; + }; + group: Measure[]; +}; + +export const buildMeasure = ( + libraryUrl: string, + measures: Measure[], +): BuildMeasureReturn => { + const measureId = uuidv4(); + return { + resourceType: "Measure", + url: "urn:uuid:" + measureId, + status: "active", + subjectCodeableConcept: { + coding: [ + { + system: "http://hl7.org/fhir/resource-types", + code: "Patient", + }, + ], + }, + library: libraryUrl, + scoring: { + coding: [ + { + system: "http://terminology.hl7.org/CodeSystem/measure-scoring", + code: "cohort", + }, + ], + }, + group: measures, // configuration.resultRequests.map(request => request.measures) + }; +}; diff --git a/packages/demo/src/backends/cqlquery-mappings.ts b/packages/demo/src/backends/cqlquery-mappings.ts new file mode 100644 index 00000000..12bcac63 --- /dev/null +++ b/packages/demo/src/backends/cqlquery-mappings.ts @@ -0,0 +1,429 @@ +export const alias = new Map([ + ["icd10", "http://fhir.de/CodeSystem/bfarm/icd-10-gm"], + ["loinc", "http://loinc.org"], + ["gradingcs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GradingCS"], + ["ops", "http://fhir.de/CodeSystem/bfarm/ops"], + ["morph", "urn:oid:2.16.840.1.113883.6.43.1"], + ["lokalisation_icd_o_3", "urn:oid:2.16.840.1.113883.6.43.1"], + [ + "bodySite", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SeitenlokalisationCS", + ], + [ + "Therapieart", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SYSTTherapieartCS", + ], + ["specimentype", "https://fhir.bbmri.de/CodeSystem/SampleMaterialType"], + [ + "uiccstadiumcs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/UiccstadiumCS", + ], + [ + "lokalebeurteilungresidualstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/LokaleBeurteilungResidualstatusCS", + ], + [ + "gesamtbeurteilungtumorstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GesamtbeurteilungTumorstatusCS", + ], + [ + "verlauflokalertumorstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufLokalerTumorstatusCS", + ], + [ + "verlauftumorstatuslymphknotencs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusLymphknotenCS", + ], + [ + "verlauftumorstatusfernmetastasencs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusFernmetastasenCS", + ], + [ + "vitalstatuscs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VitalstatusCS", + ], + ["jnucs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/JNUCS"], + [ + "fmlokalisationcs", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/FMLokalisationCS", + ], + ["TNMTCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMTCS"], + ["TNMNCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMNCS"], + ["TNMMCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMMCS"], + [ + "TNMySymbolCS", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMySymbolCS", + ], + [ + "TNMrSymbolCS", + "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMrSymbolCS", + ], + [ + "TNMmSymbolCS", + "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([ + ["gender", "Patient.gender = '{{C}}'"], + ["conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"], + [ + "conditionBodySite", + "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}", + ], + //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved + // ["conditionLocalization", "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}"], + [ + "conditionLocalization", + "exists from [Condition] C\nwhere C.bodySite.coding.code contains '{{C}}'", + ], + [ + "conditionRangeDate", + "exists from [Condition] C\nwhere year from C.onset between {{D1}} and {{D2}}", + ], + [ + "conditionLowerThanDate", + "exists from [Condition] C\nwhere year from C.onset <= {{D2}}", + ], + [ + "conditionGreaterThanDate", + "exists from [Condition] C\nwhere year from C.onset >= {{D1}}", + ], + [ + "conditionRangeAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between {{D1}} and {{D2}}", + ], + [ + "conditionLowerThanAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) <= {{D2}}", + ], + [ + "conditionGreaterThanAge", + "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) >= {{D1}}", + ], + [ + "primaryConditionRangeDate", + "year from PrimaryDiagnosis.onset between {{D1}} and {{D2}}", + ], + [ + "primaryConditionLowerThanDate", + "year from PrimaryDiagnosis.onset <= {{D2}}", + ], + [ + "primaryConditionGreaterThanDate", + "year from PrimaryDiagnosis.onset >= {{D1}}", + ], + [ + "primaryConditionRangeAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) between {{D1}} and {{D2}}", + ], + [ + "primaryConditionLowerThanAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) <= {{D2}}", + ], + [ + "primaryConditionGreaterThanAge", + "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) >= {{D1}}", + ], + //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved + // ["observation", "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding contains Code '{{C}}' from {{A2}}"], + [ + "observation", + "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "observationMetastasis", + "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'", + ], + [ + "observationMetastasisBodySite", + "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.bodySite.coding.code contains '{{C}}'", + ], + [ + "observationMolecularMarkerName", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "observationMolecularMarkerAminoacidchange", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], //TODO @ThomasK replace C with S + [ + "observationMolecularMarkerDNAchange", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + [ + "observationMolecularMarkerSeqRefNCBI", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + [ + "observationMolecularMarkerEnsemblID", + "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'", + ], + ["procedure", "exists [Procedure: category in Code '{{K}}' from {{A1}}]"], + [ + "procedureResidualstatus", + "exists from [Procedure: category in Code 'OP' from {{A1}}] P\nwhere P.outcome.coding.code contains '{{C}}'", + ], + [ + "medicationStatement", + "exists [MedicationStatement: category in Code '{{K}}' from {{A1}}]", + ], + ["hasSpecimen", "exists [Specimen]"], + ["specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"], + [ + "TNMc", + "exists from [Observation: Code '21908-9' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "TNMp", + "exists from [Observation: Code '21902-2' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}", + ], + [ + "Organization", + "Patient.managingOrganization.reference = \"Organization Ref\"('Klinisches Krebsregister/ITM')", + ], + [ + "department", + "exists from [Encounter] I\nwhere I.identifier.value = '{{C}}' ", + ], + [ + "uiccstadium", + "(exists ([Observation: Code '21908-9' from loinc] O where O.value.coding.code contains '{{C}}')) or (exists ([Observation: Code '21902-2' from loinc] O where O.value.coding.code contains '{{C}}'))", + ], + ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"], + + ["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( + [ + ["gender", { type: "gender" }], + ["histology", { type: "histology", alias: ["loinc"] }], + ["diagnosis", { type: "conditionValue", alias: ["icd10"] }], + ["bodySite", { type: "conditionBodySite", alias: ["bodySite"] }], + [ + "urn:oid:2.16.840.1.113883.6.43.1", + { type: "conditionLocalization", alias: ["lokalisation_icd_o_3"] }, + ], + ["59542-1", { type: "observation", alias: ["loinc", "gradingcs"] }], //grading + [ + "metastases_present", + { type: "observationMetastasis", alias: ["loinc", "jnucs"] }, + ], //Fernmetastasen vorhanden + [ + "localization_metastases", + { + type: "observationMetastasisBodySite", + alias: ["loinc", "fmlokalisationcs"], + }, + ], //Fernmetastasen + ["OP", { type: "procedure", alias: ["Therapieart"] }], //Operation + ["ST", { type: "procedure", alias: ["Therapieart"] }], //Strahlentherapie + ["CH", { type: "medicationStatement", alias: ["Therapieart"] }], //Chemotherapie + ["HO", { type: "medicationStatement", alias: ["Therapieart"] }], //Hormontherapie + ["IM", { type: "medicationStatement", alias: ["Therapieart"] }], //Immuntherapie + ["KM", { type: "medicationStatement", alias: ["Therapieart"] }], //Knochenmarktransplantation + ["59847-4", { type: "observation", alias: ["loinc", "morph"] }], //Morphologie + ["year_of_diagnosis", { type: "conditionRangeDate" }], + ["year_of_primary_diagnosis", { type: "primaryConditionRangeDate" }], + ["sample_kind", { type: "specimen", alias: ["specimentype"] }], + ["pat_with_samples", { type: "hasSpecimen" }], + ["age_at_diagnosis", { type: "conditionRangeAge" }], + ["age_at_primary_diagnosis", { type: "primaryConditionRangeAge" }], + ["21908-9", { type: "uiccstadium", alias: ["loinc", "uiccstadiumcs"] }], + ["21905-5", { type: "TNMc", alias: ["loinc", "TNMTCS"] }], //tnm component + ["21906-3", { type: "TNMc", alias: ["loinc", "TNMNCS"] }], //tnm component + ["21907-1", { type: "TNMc", alias: ["loinc", "TNMMCS"] }], //tnm component + ["42030-7", { type: "TNMc", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component + ["59479-6", { type: "TNMc", alias: ["loinc", "TNMySymbolCS"] }], //tnm component + ["21983-2", { type: "TNMc", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component + ["21899-0", { type: "TNMp", alias: ["loinc", "TNMTCS"] }], //tnm component + ["21900-6", { type: "TNMp", alias: ["loinc", "TNMNCS"] }], //tnm component + ["21901-4", { type: "TNMp", alias: ["loinc", "TNMMCS"] }], //tnm component + ["42030-7", { type: "TNMp", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component + ["59479-6", { type: "TNMp", alias: ["loinc", "TNMySymbolCS"] }], //tnm component + ["21983-2", { type: "TNMp", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component + + ["Organization", { type: "Organization" }], //organization + [ + "48018-6", + { + type: "observationMolecularMarkerName", + alias: ["loinc", "molecularMarker"], + }, + ], //molecular marker name + [ + "48005-3", + { + type: "observationMolecularMarkerAminoacidchange", + alias: ["loinc"], + }, + ], //molecular marker + [ + "81290-9", + { type: "observationMolecularMarkerDNAchange", alias: ["loinc"] }, + ], //molecular marker + [ + "81248-7", + { type: "observationMolecularMarkerSeqRefNCBI", alias: ["loinc"] }, + ], //molecular marker + [ + "81249-5", + { type: "observationMolecularMarkerEnsemblID", alias: ["loinc"] }, + ], //molecular marker + + [ + "local_assessment_residual_tumor", + { + type: "procedureResidualstatus", + alias: ["Therapieart", "lokalebeurteilungresidualstatuscs"], + }, + ], //lokalebeurteilungresidualstatuscs + [ + "21976-6", + { + type: "observation", + alias: ["loinc", "gesamtbeurteilungtumorstatuscs"], + }, + ], //GesamtbeurteilungTumorstatus + [ + "LA4583-6", + { + type: "observation", + alias: ["loinc", "verlauflokalertumorstatuscs"], + }, + ], //LokalerTumorstatus + [ + "LA4370-8", + { + type: "observation", + alias: ["loinc", "verlauftumorstatuslymphknotencs"], + }, + ], //TumorstatusLymphknoten + [ + "LA4226-2", + { + type: "observation", + alias: ["loinc", "verlauftumorstatusfernmetastasencs"], + }, + ], //TumorstatusFernmetastasen + ["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 new file mode 100644 index 00000000..fa1f0d38 --- /dev/null +++ b/packages/demo/src/backends/spot.ts @@ -0,0 +1,107 @@ +/** + * TODO: document this class + */ + +import type { + ResponseStore, + SiteData, + Status, + BeamResult, +} from "../../../../dist/types"; + +export class Spot { + private currentTask!: string; + + constructor( + private url: URL, + private sites: Array, + ) {} + + /** + * sends the query to beam and updates the store with the results + * @param query the query as base64 encoded string + * @param updateResponse the function to update the response store + * @param controller the abort controller to cancel the request + */ + async send( + query: string, + updateResponse: (response: ResponseStore) => void, + controller: AbortController, + ): Promise { + try { + this.currentTask = crypto.randomUUID(); + const beamTaskResponse = await fetch( + `${this.url}beam?sites=${this.sites.toString()}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: import.meta.env.PROD ? "include" : "omit", + body: JSON.stringify({ + id: this.currentTask, + sites: this.sites, + query: 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.`); + } + + console.info(`Created new Beam Task with id ${this.currentTask}`); + + const eventSource = new EventSource( + `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`, + { + withCredentials: true, + }, + ); + + /** + * 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; + + const parsedResponse: ResponseStore = new Map().set(site, { + status: status, + data: body, + }); + updateResponse(parsedResponse); + }); + + // read error events from beam + eventSource.addEventListener("error", (message) => { + console.error(`Beam returned error ${message}`); + eventSource.close(); + }); + + // 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}`); + } else { + console.error(err); + } + } + } +} 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..92f73e87 100644 --- a/packages/demo/src/main.ts +++ b/packages/demo/src/main.ts @@ -18,7 +18,7 @@ import App from "./AppCCP.svelte"; // import './gba.css' const app = new App({ - target: document.getElementById("app"), + target: document.getElementById("app") as HTMLElement, }); export default app; diff --git a/packages/lib/src/classes/blaze.ts b/packages/lib/src/classes/blaze.ts index a475acd7..702536b2 100644 --- a/packages/lib/src/classes/blaze.ts +++ b/packages/lib/src/classes/blaze.ts @@ -1,18 +1,12 @@ 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); -}); +import type { Site, SiteData } from "../types/response"; +import type { Measure, ResponseStore } from "../types/backend"; export class Blaze { constructor( private url: URL, private name: string, + private updateResponse: (response: ResponseStore) => void, private auth: string = "", ) {} @@ -20,13 +14,21 @@ export class Blaze { * 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 + * @param measureDefinitions the measure definitions to send to blaze */ - async send(cql: string, controller?: AbortController): Promise { + async send( + cql: string, + controller: AbortController, + measureDefinitions: Measure[], + ): Promise { try { - responseStore.update((store) => { - store.set(this.name, { status: "claimed", data: null }); - return store; - }); + let response: ResponseStore = new Map().set( + this.name, + { status: "claimed", data: {} as SiteData }, + ); + + this.updateResponse(response); + const libraryResponse = await fetch( new URL(`${this.url}/Library`), { @@ -80,15 +82,15 @@ export class Blaze { ); } const blazeResponse: Site = await dataResponse.json(); - responseStore.update((store) => { - store.set(this.name, { - status: "succeeded", - data: blazeResponse, - }); - return store; + + response = new Map().set(this.name, { + status: "succeeded", + data: blazeResponse.data, }); + + this.updateResponse(response); } catch (err) { - if (err.name === "AbortError") { + if (err instanceof Error && err.name === "AbortError") { console.log(`Aborting former blaze request.`); } else { console.error(err); @@ -101,9 +103,11 @@ export class Blaze { console.debug( `${message}. Received error ${response.status} with message ${errorMessage}`, ); - responseStore.update((store) => { - store.set(this.name, { status: "permfailed", data: null }); - return store; - }); + + const failedResponse: ResponseStore = new Map().set( + this.name, + { status: "permfailed", data: null }, + ); + this.updateResponse(failedResponse); } } diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts index 11c76bf8..e0a312d9 100644 --- a/packages/lib/src/classes/spot.ts +++ b/packages/lib/src/classes/spot.ts @@ -2,24 +2,9 @@ * TODO: document this class */ -import { responseStore } from "../stores/response"; -import type { ResponseStore } from "../types/backend"; - import type { SiteData, Status } from "../types/response"; +import type { ResponseStore } from "../types/backend"; -type BeamResult = { - body: string; - from: string; - metadata: string; - status: Status; - task: string; - to: string[]; -}; - -/** - * Implements requests to multiple targets through the middleware spot (see: https://github.com/samply/spot). - * The responses are received via Server Sent Events - */ export class Spot { private currentTask!: string; @@ -31,9 +16,14 @@ export class Spot { /** * sends the query to beam and updates the store with the results * @param query the query as base64 encoded string + * @param updateResponse the function to update the response store * @param controller the abort controller to cancel the request */ - async send(query: string, controller?: AbortController): Promise { + async send( + query: string, + updateResponse: (response: ResponseStore) => void, + controller: AbortController, + ): Promise { try { this.currentTask = crypto.randomUUID(); const beamTaskResponse = await fetch( @@ -62,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}`, { @@ -78,10 +71,11 @@ export class Spot { ? JSON.parse(atob(response.body)) : null; - 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 5d9676a4..c321aeb5 100644 --- a/packages/lib/src/components/DataPasser.wc.svelte +++ b/packages/lib/src/components/DataPasser.wc.svelte @@ -6,8 +6,8 @@ diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte index 5651ff73..600ac086 100644 --- a/packages/lib/src/components/Options.wc.svelte +++ b/packages/lib/src/components/Options.wc.svelte @@ -15,20 +15,23 @@ */ import { lensOptions } from "../stores/options"; import { catalogue } from "../stores/catalogue"; + import { measureStore } from "../stores/measures"; import { iconStore } from "../stores/icons"; + import type { MeasureStore } from "../types/backend"; import type { Criteria } from "../types/treeData"; - import optionsSchema from "../interfaces/options.schema.json"; - import catalogueSchema from "../interfaces/catalogue.schema.json"; + import optionsSchema from "../types/options.schema.json"; + import catalogueSchema from "../types/catalogue.schema.json"; import { parser } from "@exodus/schemasafe"; 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; /** * Validate the options against the schema before passing them to the store */ - $: { const parse = parser(optionsSchema, { includeErrors: true, @@ -60,7 +63,10 @@ ); } } - + /** + * 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) { @@ -103,7 +109,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; + Object.entries(options?.siteMappings)?.forEach((site) => { + mappings.set(site[0], site[1]); + }); + + return mappings; + }); + $: $lensOptions = options; $: updateIconStore(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..c84ddf1a 100644 --- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte +++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte @@ -16,57 +16,50 @@ import { translateAstToCql } from "../../cql-translator-service/ast-to-cql-translator"; import { buildLibrary, buildMeasure } from "../../helpers/cql-measure"; import { Spot } from "../../classes/spot"; - import { - catalogueKeyToResponseKeyMap, - uiSiteMappingsStore, - } from "../../stores/mappings"; - import type { Measure, BackendConfig } from "../../types/backend"; - import { responseStore } from "../../stores/response"; + import { Blaze } from "../../classes/blaze"; + import { catalogueKeyToResponseKeyMap } from "../../stores/mappings"; + import { responseStore, updateResponseStore } from "../../stores/response"; + import { lensOptions } from "../../stores/options"; + import type { + BackendOptions, + BlazeOption, + Measure, + MeasureItem, + MeasureOption, + SpotOption, + } from "../../types/backend"; + import type { AstTopLayer } from "../../types/ast"; + import type { Site } from "../../types/response"; 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 = ""; - let controller: 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) => { - backendConfig.uiSiteMap.forEach((site) => { - mappings.set(site[0], site[1]); - }); - return mappings; - }); + $: options = $lensOptions?.backends as BackendOptions; + + let controller: AbortController = new AbortController(); $: 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]); + }); + }); + options?.blazes?.forEach((blaze: BlazeOption) => { + blaze.catalogueKeyToResponseKeyMap.forEach((mapping) => { + mappings.set(mapping[0], mapping[1]); + }); }); return mappings; }); /** - * watches the measures for changes to populate the measureStore + * 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. */ - $: measureStore.set(measures); - - /** - * triggers a request to the backend via the spot class - */ - const getResultsFromBackend = async (): void => { + const getResultsFromBackend = (): void => { if (controller) { controller.abort(); } @@ -75,24 +68,128 @@ 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) => { + const name = spot.name; + const measureItem: MeasureOption | undefined = $measureStore.find( + (measureStoreItem: MeasureOption) => + spot.name === measureStoreItem.name, + ); + + 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(new URL(spot.url), spot.sites); + + backend.send( + btoa(decodeURI(JSON.stringify(query))), + updateResponseStore, + controller, + ); + }); + + options?.blazes?.forEach((blaze: BlazeOption) => { + const { + name, + url, + backendMeasures, + }: { name: string; url: string; backendMeasures: string } = blaze; + + const measureItem: MeasureOption | undefined = $measureStore.find( + (measureStoreItem: MeasureOption) => + name === measureStoreItem.name, + ); + + if (measureItem === undefined) { + throw new Error( + `No measures found for backend ${name}. Please check the measures store.`, + ); + } - const backend = new Spot( - new URL(backendConfig.url), - backendConfig.backends, - ); + const measures: Measure[] = measureItem.measures.map( + (measureItem: MeasureItem) => measureItem.measure, + ); - backend.send(btoa(decodeURI(JSON.stringify(query))), controller); + const cql = translateAstToCql( + ast, + false, + backendMeasures, + measureItem.measures, + ); + + const backend = new Blaze(new URL(url), name, updateResponseStore); + + backend.send(cql, controller, measures); + }); + + options?.customAstBackends?.forEach((customAstBackendUrl: string) => { + customBackendCallWithAst(ast, customAstBackendUrl); + }); + emitEvent(ast); 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", + 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; + 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); + };